48870 (Розробка власного класу STRING), страница 3

2016-07-30СтудИзба

Описание файла

Документ из архива "Розробка власного класу STRING", который расположен в категории "". Всё это находится в предмете "информатика" из 1 семестр, которые можно найти в файловом архиве . Не смотря на прямую связь этого архива с , его также можно найти и в других разделах. Архив можно найти в разделе "курсовые/домашние работы", в предмете "информатика, программирование" в общих файлах.

Онлайн просмотр документа "48870"

Текст 3 страницы из документа "48870"

cout << "count = " << count

"", increment = " " increment << endl; }

main ()

{

Increment value (10,5);

cout << "Перед збільшенням: "; value. print ();

for (int j = 1; j <= 3;) }

value. addlncrement ();

cout << "Після збільшення " << j "": "; value. print ();

}

return 0; }

Перед збільшенням: count = 10, increment = 5

Після збільшення 1: count = 15, increment = 5

Після збільшення 2: count = 20, increment = 5

Після збільшення 3: count = 25, increment = 5

Мал.7. Використання ініціалізаторів елементів для ініціалізації даних константного типу убудованого типу


1.13 Друзі

Нехай визначені два класи: vector (вектор) і matrix (матриця). Кожний з них приховує своє подання даних, але дає повний набір операцій для роботи з об'єктами його типу. Допустимо, треба визначити функцію, що множить матрицю на вектор. Для простоти припустимо, що вектор має чотири елементи з індексами від 0 до 3, а в матриці чотири вектори теж з індексами від 0 до 3. Доступ до елементів вектора забезпечується функцією elem (), і аналогічна функція є для матриці. Можна визначити глобальну функцію multiply (помножити) у такий спосіб:

vector multiply (const matrix& m, const vector& v);

{

vector r;

for (int i = 0; i<3; i++) { // r [i] = m [i] * v;

r. elem (i) = 0;

for (int j = 0; j<3; j++)

r. elem (i) +=m. elem (i,j) * v. elem (j);

}

return r;

}

Це цілком природнє рішення, але воно може виявитися дуже неефективним. При кожному виклику multiply () функція elem () буде викликатися 4* (1+4*3) раз. Якщо в elem () проводиться контроль границь масиву, то на такий контроль буде витрачено значно більше часу, ніж на виконання самої функції, і в результаті вона виявиться непридатної для користувачів. З іншого боку, якщо elem () є якийсь спеціальний варіант доступу без контролю, то тим самим ми засмічуємо інтерфейс із вектором і матрицею особливою функцією доступу, що потрібна тільки для обходу контролю.

Якщо можна було б зробити multiply членом обох класів vector і matrix, ми могли б обійтися без контролю індексу при звертанні до елемента матриці, але в той же час не вводити спеціальної функції elem (). Однак, функція не може бути членом двох класів. Треба мати в мові можливість надавати функції, що не є членом, право доступу до приватних членів класу. Функція - не член класу, але має доступ до його закритої частини, називається другом цього класу. Функція може стати другом класу, якщо в його описі вона описана як friend (друг). Наприклад:

class matrix;

class vector {

float v [4];

// ...

friend vector multiply (const matrix&, const vector&);

};

class matrix {

vector v [4];

// ...

friend vector multiply (const matrix&, const vector&);

};

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

Тепер можна написати функцію multiply, використовуючи елементи вектора й матриці безпосередньо:

vector multiply (const matrix& m, const vector& v)

{

vector r;

for (int i = 0; i<3; i++) { // r [i] = m [i] * v;

r. v [i] = 0;

for (int j = 0; j<3; j++)

r. v [i] +=m. v [i] [j] * v. v [j];

}

return r;

}

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

Функція-член одного класу може бути другом іншого класу:

class x {

// ...

void f ();

};

class y {

// ...

friend void x:: f ();

};

Цілком можливо, що всі функції одного класу є друзями іншого класу. Для цього є коротка форма запису:

class x {

friend class y;

// ...

};

У результаті такого опису всі функції-члени y стають друзями класу x.

1.14 Ядро ООП: Успадкування та поліморфізм

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


1.4.1 Похідні класи

Обговоримо, як написати програму обліку службовців деякої фірми. У ній може використатися, наприклад, така структура даних:

struct employee { // службовець

char* name; // ім'я

short age; // вік

short department; // відділ

int salary; // оклад

employee* next;

// ...

};

Поле next потрібно для зв'язування в список записів про службовців одного відділу (employee). Тепер спробуємо визначити структуру даних для керуючого (manager):

struct manager {

employee emp; // запис employee для керуючого

employee* group; // підлеглий колектив

short level;

// ...

};

Керуючий також є службовцем, тому запис employee зберігається в члені emp об'єкта manager. Для людини ця спільність очевидна, але для транслятора член emp нічим не відрізняється від інших членів класу. Вказівник на структуру manager (manager*) не є вказівником на employee (employee*), тому не можна вільно використати один замість іншого. Зокрема, без спеціальних дій не можна об'єкт manager включити до списку об'єктів типу employee. Доведеться або використати явне приведення типу manager*, або в список записів employee включити адресу члена emp. Обоє рішень некрасиві й можуть бути досить заплутаними. Правильне рішення полягає в тому, щоб тип manager був типом employee з деякою додатковою інформацією:

struct manager: employee {

employee* group;

short level;

// ...

};

Клас manager є похідним від employee, і, навпаки, employee є базовим класом для manager. Крім члена group у класі manager є члени класу employee (name, age і т.д.). Графічно відношення спадкування звичайно зображується у вигляді стрілки від похідних класів до базового:

employee

manager

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

Маючи визначення employee і manager, можна створити список службовців, частина з яких є й керуючими:

void f ()

{

manager m1, m2;

employee e1, e2;

employee* elist;

elist = &m1; // помістити m1 в elist

m1. next = &e1; // помістити e1 в elist

e1. next = &m2; // помістити m2 в elist

m2. next = &e2; // помістити m2 в elist

e2. next = 0; // кінець списку

}

Оскільки керуючий є також службовцем, вказівник manager* можна використати як employee*. У той же час службовець не обов'язково є керуючим, і тому employee* не можна використати як manager*.

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

void g ()

{

manager mm;

employee* pe = &mm; // нормально

employee ee;

manager* pm = ⅇ // помилка:

// не всякий службовець є керуючим

pm->level = 2; // катастрофа: при розміщенні ee

// пам'ять для члена 'level' не виділялася

pm = (manager*) pe; // нормально: насправді pe

// не настроєно на об'єкт mm типу manager

pm->level = 2; // відмінно: pm указує на об'єкт mm

// типу manager, а в ньому при розміщенні

// виділена пам'ять для члена 'level'

}

Іншими словами, якщо робота з об'єктом похідного класу йде через вказівник, то його можна розглядати як об'єкт базового класу. Зворотне невірно. Відзначимо, що у звичайній реалізації С++ не передбачається динамічного контролю над тим, щоб після перетворення типу, подібного тому, що використовувалося в присвоюванні pe в pm, отриманий у результаті вказівник дійсно був налаштований на об'єкт необхідного типу.

1.14.2 Функції-члени

Прості структури даних начебто employee і manager самі по собі не занадто цікаві, а часто й не дуже корисні. Тому додамо до них функції:

class employee {

char* name;

// ...

public:

employee* next; // перебуває в загальній частині, щоб

// можна було працювати зі списком

void print () const;

// ...

};

class manager: public employee {

// ...

public:

void print () const;

// ...

};

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

Розглянемо приклад:

void manager:: print () const

{

cout << " ім'я " << name << '\n';

}

Член похідного класу може використати ім'я із загальної частини свого базового класу нарівні з усіма іншими членами, тобто без вказівки імені об'єкта. Передбачається, що є об'єкт, на який настроєний this, тому коректним звертанням до name буде this->name. Однак, при трансляції функції manager:: print () буде зафіксована помилка: члену похідного класу не надане право доступу до приватних членів його базового класу, значить name недоступно в цій функції.

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

Як правило, саме надійне рішення для похідного класу - використати тільки загальні члени свого базового класу:

void manager:: print () const

{

employee:: print (); // друк даних про службовців

// друк даних про керуючих

}

Відзначимо, що операція:: необхідна, оскільки функція print () перевизначена в класі manager. Таке повторне використання імен типово для С++. Необережний програміст написав би:

void manager:: print () const

{

print (); // печатка даних про службовців

// печатка даних про керуючих

}

У результаті він одержав би рекурсивну послідовність викликів manager:: print ().

1.14.3 Конструктори й деструктори

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

class employee {

// ...

public:

// ...

employee (char* n, int d);

};

class manager: public employee {

// ...

public:

// ...

manager (char* n, int i, int d);

};

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

manager:: manager (char* n, int l, int d)

: employee (n,d), level (l), group (0)

{

}

Конструктор базового класу employee:: employee () може мати таке визначення:

employee:: employee (char* n, int d)

: name (n), department (d)

{

next = list;

list = this;

}

Тут list повинен бути описаний як статичний член employee.

Об'єкти класів створюються знизу вверх: спочатку базові, потім члени й, нарешті, самі похідні класи. Знищуються вони у зворотному порядку: спочатку самі похідні класи, потім члени, а потім базові. Члени й базові створюються в порядку опису їх у класі, а знищуються вони у зворотному порядку.

1.14.4 Ієрархія класів

Похідний клас сам у свою чергу може бути базовим класом:

class employee {/*... */ };

class manager: public employee {/*... */ };

class director: public manager {/*... */ };

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

class temporary {/*... */ };

class secretary: public employee {/*... */ };

class tsec

: public temporary, public secretary { /*... */ };

class consultant

: public temporary, public manager { /*... */ };

Бачимо, що класи в С++ можуть утворювати спрямований ациклічний граф.


1.14.5 Поля типу

Щоб похідні класи були не просто зручною формою короткого опису, у реалізації мови повинно бути вирішено питання: якому з похідних класів ставиться об'єкт, на який дивиться вказівник base*? Існує три основних способи відповіді:

[1] Забезпечити, щоб вказівник міг посилатися на об'єкти тільки одного типу;

[2] Помістити в базовий клас поле типу, що зможе перевіряти функції;

[3] використати віртуальні функції.

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

Способи [2] і [3] дозволяють створювати різнорідні списки, тобто списки об'єктів декількох різних типів (насправді, списки вказівників на ці об'єкти).

Спосіб [3] - це спеціальний надійний у сенсі типу варіант спосіб [2]. Особливо цікаві й потужні варіанти дають комбінації способів [1] і [3].

Спочатку обговоримо простий спосіб з полем типу, тобто спосіб [2]. Приклад із класами manager/employee можна перевизначити так:

struct employee {

enum empl_type { M, E };

empl_type type;

employee* next;

char* name;

short department;

// ...

};

struct manager: employee {

employee* group;

short level;

// ...

};

Маючи ці визначення, можна написати функцію, що друкує дані про довільного службовця:

void print_employee (const employee* e)

{

switch (e->type) {

case E:

cout

// ...

break;

case M:

cout

// ...

manager* p = (manager*) e;

cout << "level" <

level << '\n';

// ...

break;

}

}

Надрукувати список службовців можна так:

void f (const employee* elist)

{

for (; elist; elist=elist->next) print_employee (elist);

}

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

void print (const employee* e)

{

cout

// ...

if (e->type == M) {

manager* p = (manager*) e;

cout << "level" <

level << '\n';

// ...

}

}

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

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

1.14.6 Віртуальні функції

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

class employee {

char* name;

short department;

// ...

employee* next;

static employee* list;

public:

employee (char* n, int d);

// ...

static void print_list ();

virtual void print () const;

};

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

void employee:: print () const

{

cout << name << '\t' << department << '\n';

// ...

}

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

class manager: public employee {

employee* group;

short level;

// ...

public:

manager (char* n, int d);

// ...

void print () const;

};

Місце функції print_employee () зайняли функції-члени print (), і вона стала не потрібна. Список службовців будує конструктор employee. Надрукувати його можна так:

void employee:: print_list ()

{

for (employee* p = list; p; p=p->next) p->print ();

}

Дані про кожного службовця будуть друкуватися відповідно до типу запису про нього. Тому програма

int main ()

{

employee e ("J. Brown",1234);

manager m ("J. Smith",2,1234);

employee:: print_list ();

}

надрукує

J. Smith 1234

level 2

J. Brown 1234

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

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

1.14.7 Абстрактні класи

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

class shape {

// ...

public:

virtual void rotate (int) { error ("shape:: rotate"); }

virtual void draw () { error ("shape:: draw"): }

// не можна не обертати, не малювати абстрактну фігуру

// ...

};

Створення об'єкта типу shape (абстрактної фігури) законна, хоча зовсім безглузда операція:

shape s; // нісенітниця: ''фігура взагалі''

Вона безглузда тому, що будь-яка операція з об'єктом s приведе до помилки.

Краще віртуальні функції класу shape описати як чисто віртуальні. Зробити віртуальну функцію чисто віртуальної можна, додавши ініціалізатор = 0:

class shape {

// ...

public:

virtual void rotate (int) = 0; // чисто віртуальна функція

virtual void draw () = 0; // чисто віртуальна функція

};

Клас, у якому є віртуальні функції, називається абстрактним. Об'єкти такого класу створити не можна:

shape s; // помилка: змінна абстрактного класу shape

Абстрактний клас можна використати тільки в якості базового для іншого класу:

class circle: public shape {

int radius;

public:

void rotate (int) { } // нормально:

// перевизначення shape:: rotate

void draw (); // нормально:

// перевизначення shape:: draw

circle (point p, int r);

};

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

class X {

Свежие статьи
Популярно сейчас
Как Вы думаете, сколько людей до Вас делали точно такое же задание? 99% студентов выполняют точно такие же задания, как и их предшественники год назад. Найдите нужный учебный материал на СтудИзбе!
Ответы на популярные вопросы
Да! Наши авторы собирают и выкладывают те работы, которые сдаются в Вашем учебном заведении ежегодно и уже проверены преподавателями.
Да! У нас любой человек может выложить любую учебную работу и зарабатывать на её продажах! Но каждый учебный материал публикуется только после тщательной проверки администрацией.
Вернём деньги! А если быть более точными, то автору даётся немного времени на исправление, а если не исправит или выйдет время, то вернём деньги в полном объёме!
Да! На равне с готовыми студенческими работами у нас продаются услуги. Цены на услуги видны сразу, то есть Вам нужно только указать параметры и сразу можно оплачивать.
Отзывы студентов
Ставлю 10/10
Все нравится, очень удобный сайт, помогает в учебе. Кроме этого, можно заработать самому, выставляя готовые учебные материалы на продажу здесь. Рейтинги и отзывы на преподавателей очень помогают сориентироваться в начале нового семестра. Спасибо за такую функцию. Ставлю максимальную оценку.
Лучшая платформа для успешной сдачи сессии
Познакомился со СтудИзбой благодаря своему другу, очень нравится интерфейс, количество доступных файлов, цена, в общем, все прекрасно. Даже сам продаю какие-то свои работы.
Студизба ван лав ❤
Очень офигенный сайт для студентов. Много полезных учебных материалов. Пользуюсь студизбой с октября 2021 года. Серьёзных нареканий нет. Хотелось бы, что бы ввели подписочную модель и сделали материалы дешевле 300 рублей в рамках подписки бесплатными.
Отличный сайт
Лично меня всё устраивает - и покупка, и продажа; и цены, и возможность предпросмотра куска файла, и обилие бесплатных файлов (в подборках по авторам, читай, ВУЗам и факультетам). Есть определённые баги, но всё решаемо, да и администраторы реагируют в течение суток.
Маленький отзыв о большом помощнике!
Студизба спасает в те моменты, когда сроки горят, а работ накопилось достаточно. Довольно удобный сайт с простой навигацией и огромным количеством материалов.
Студ. Изба как крупнейший сборник работ для студентов
Тут дофига бывает всего полезного. Печально, что бывают предметы по которым даже одного бесплатного решения нет, но это скорее вопрос к студентам. В остальном всё здорово.
Спасательный островок
Если уже не успеваешь разобраться или застрял на каком-то задание поможет тебе быстро и недорого решить твою проблему.
Всё и так отлично
Всё очень удобно. Особенно круто, что есть система бонусов и можно выводить остатки денег. Очень много качественных бесплатных файлов.
Отзыв о системе "Студизба"
Отличная платформа для распространения работ, востребованных студентами. Хорошо налаженная и качественная работа сайта, огромная база заданий и аудитория.
Отличный помощник
Отличный сайт с кучей полезных файлов, позволяющий найти много методичек / учебников / отзывов о вузах и преподователях.
Отлично помогает студентам в любой момент для решения трудных и незамедлительных задач
Хотелось бы больше конкретной информации о преподавателях. А так в принципе хороший сайт, всегда им пользуюсь и ни разу не было желания прекратить. Хороший сайт для помощи студентам, удобный и приятный интерфейс. Из недостатков можно выделить только отсутствия небольшого количества файлов.
Спасибо за шикарный сайт
Великолепный сайт на котором студент за не большие деньги может найти помощь с дз, проектами курсовыми, лабораторными, а также узнать отзывы на преподавателей и бесплатно скачать пособия.
Популярные преподаватели
Добавляйте материалы
и зарабатывайте!
Продажи идут автоматически
5167
Авторов
на СтудИзбе
437
Средний доход
с одного платного файла
Обучение Подробнее