Популярные услуги

Все письменные КМ под ключ за 3 суток! (КМ-6 + КМ-7 + КМ-8 + КМ-9 + КМ-10)
КМ-6. Динамические массивы. Семинар - выполню любой вариант!
КМ-2. Разработка простейших консольных программ с использованием ООП + КМ-4. Более сложные элементы ООП - под ключ!
Любая задача на C/C++
Одно любое задание в mYsql
Сделаю ваше задание: Лабораторная работа на Pascal / Lazarus
Любой тест по базам данных максимально быстро на хорошую оценку - или верну деньги!
Любой реферат по объектно-ориентированному программированию (ООП)
Повышение уникальности твоей работе
Оба семинара по программированию под ключ! КМ-2. Разработка циклических алгоритмов + КМ-3. Функции и многофайловые программы в Си

Лекция 18

2021-03-09СтудИзба

                                    Лекция 18

Часть II:

Объектно-ориентированные языки

программирования (ООЯП)

            Идут очень интенсивные споры по поводу того, что составляет предмет ОО проектирования.

            У нас концепция: какие черты у ЯП, чтобы он мог называться ООЯП?

            3 составляющие:

1. Инкапсуляция (для построения больших систем)

2. Наследование

Рекомендуемые материалы

3. Динамический полиморфизм

Во многих ЯП существует статический полиморфизм: одной сущности (подпрограмма) может соответствовать несколько форм (несколько тел). Это overloading (перегрузка). Связывание (тела функции с её вызовом) происходит в момент компиляции. Хотя одной сущности может соответствовать несколько тел (одному имени- несколько функций). Динамический полиморфизм должен быть в ООЯП (без него наследование особого смысла не имеет).

[Книга Себесты] ЯП должны поддерживать концепции:

                        АТД

                        наследование

                        динамическое связывание

- то же самое, но другими словами

Про инкапсуляцию поговорили в первой части нашего курса.

Наследование уже появляется в традиционных ЯП. Понятие наследования есть в Аде

                       

                        package M is

                                   type T is          /* набор операций */

                                   procedure P(x: inout T);

                                               ...

                        end M;

            Соответствующий пакет называется определяющим для ТД.

            Где-то можем отдельно компилированную единицу определить:

                        with M; use M;

            Написать свой модуль-пакет:

                        package MM is

                                   type T1 is new T;

{можем ввести свой новый ТД. Здесь может стоять какое-то уточнение, если тип данных Т был описан в виде какой-то структуры, или, например, неограниченный массив; но чаще в Т идёт речь о каком-то приватном ТД (так что никакого уточнения структуры быть не может), но что известно, что ТД Т1 обладает тем же набором операций, что и Т. Единственное, что здесь мы можем добавлять свои новые операции:}

                                   procedure PP(x : T1);

            {можем написать что-то ещё или можем переопределить процедуру P}

                        procedure P(x: inout T);

            То есть какое-то наследование есть. Но тут идёт речь именно о наследовании множества операций. Происходит полное наследование структуры и полное наследование множества операций. Операции при наследовании можем добавлять или переопределять, но структуру типа Т1, по сравнению с типом Т мы поменять не можем. Это вырожденный случай наследования.

            ООЯП используют другую концепцию наследования. Обогащать и изменять можно не только набор операций, но и структуру.

                        Глава 1: наследование

            Общая идея наследования: у нас есть некий базовый ТД, который называется базовым классом (суперкласс в терминологии языка SmallTalk). К тому же, у нас есть некий механизм наследования, который из базового типа Т выводит Т1; где Т- это базовый класс или суперкласс(в Java вызов метода суперкласса- ключевое слово super) , а Т1- это производный класс (такую терминологию ввёл Страуструп в языке С++) или подкласс. При этом Т1 содержит все члены (и члены-данные и члены-функции) Т, но добавляет ещё новые члены, поэтому Страуструпу не понравилось слово «подкласс», так как «под»- это что-то меньшее. А тут ситуация немножко наоборот: производный класс расширяет. При наследовании синтаксис может быть разный, но суть одна.

            Оберон (самый простой ООЯП): базовым классом может быть любой класс- запись.

            Две категории ЯП:

                        1)Есть специальные ТД, которые можно расширять

                        2)Единственный ТД можно расширять

                        TYPE T = RECORD

                                   X: INTEGER

                        END;

                        T1 = RECORD (T)

                                   Y: INTEGER

                        END;

                        A: T;    B: T1;

            Можно писать:

                        A.X

                        B.X, B.Y

            Можно расширять любой ТД, типа запись.

            Delphi:

                        class T;

                        type T1 =

                                   class (T)

                                               ...

                                   end;

            В Delphi, с точки зрения расширения, подход несколько иной. Для обеспечения совместимости со старыми языками (Турбо Паскаль): расширять можно только класс (обычные записи расширять нельзя). То есть, просто со времён Турбо Паскаля 5.5 была добавлена новая совокупность типов данных (Турбо Паскаль- ключевое слово object, Delphi- class). В Delphi есть базовый выделенный класс (Тobject). Если мы пишем:

                        type X =

                                   class

                                               ...

                                   end;

            Это аналогично:

                        type X =

                                   class (T: Object)

                                               ...

                                   end;

                        {из терминологии языка SmallTalk}

В Delphi - это TObject, в Java - Object, С# - object. Это специальный класс, про который компилятор кое-что знает: в частности, то, что любой другой класс, даже, если не указано наследование, он всегда, по умолчанию будет выводиться, из этого базисного класса. В Обероне отсутствует понятие базисного ТД, на зато можно расширять произвольный ТД типа запись. В Java и С# нет понятия запись, зато есть понятие структура в C#, которая кроме отсутствия ссылочной семантики отличается от класса, именно тем, что от неё ничего нельзя наследовать, но, опять же, по умолчанию, ТД структура наследуется от object и по этому обладает всеми свойствами. Базисный ТД обладает только функциями(интерфейсом), но не данными.

            В языках, которые объектную ориентированность добавляли к уже существующим процедурным парадигмам,- C++ и Ада 95, которые тоже являются настоящими ООЯП, необходимы были дополнительные средства, сопряженные с существующей концепцией типа. Поэтому там не существует никакого супер базисного ТД, из которого выводятся все остальные.

            С++: есть понятие класса, и из любого класса можно вывести другой. Структуры трактуются, как частный случай класса, то есть отсутствие класса. В С++, с точки зрения управления наследования, более хитрая схема:

                       

                        сlass X {

                                   ...

                        };

- не является наследником, если не указано наследование, а от этого класса уже можно наследовать.

class Y: базовый класс { /* дополнительные члены */ };

При этом сложность в том, что описание базового класса выглядит так: [модификатор доступа] имя класса. И из стандартных ТД наследовать нельзя. Но во многих библиотеках существует базовый класс, принадлежащий библиотеке, а не языку, из которого всё и выводится (В борландовских библиотеках- это TObject), так как наличие базового супер класса является преимуществом. Модификатор доступа может отсутствовать, по умолчанию: если у нас ключевое слово class, то это private, если structure- public. При этом есть ещё и protected: он управляет доступом к членам базовых классов с точки зрения производных классов. Эти модификаторы в применении к наследованию- это модификация правил доступа в базовом классе:

                        class X {

                                    public:

                                               int i;

                                    protected:

                                                int j;

                                    private

                                                int k;

}

class Y: public X {

                                    void f() {

                                               i = 0;

                                               j = 1;

                                               k = 2;              - нельзя

                                   };

                        }

            Если модификатор public, то какие права доступа объявлены в классе Х, такими они и сохраняются в классе Y. И если где-то есть функция g():

            void g() {X a; Y: b;

можно писать:

a.i, b.i,

нельзя писать:

             a.j, a.k, b.j, b.k

            }

Если class Y: protected X, то публичные члены становятся защищёнными, защищённые остаются защищёнными, а приватные остаются приватными. Модификатор может только понизить права доступа к соответствующему классу. Следовательно теперь обращение b.i становится некорректным.

Если class Y: private X, то даже защищённые члены Х становятся приватными. То есть, хотя мы в классе Y имели право обращаться к j:

           

class Z: public Y

                        j = 1;

            Аналогичным образом работает концепция друзей:

                        class X {

                                   friend class T;

                                   ...

                        }

                        void T::g() {X x; Y y;

                        x.k- можно; y.k- нельзя (Y не разрешил дружбу с Т). То есть для продолжения отношений класс Y должен явно объявить дружбу с T. Дружба не транзитивна.

            При наследовании в С++ очень гибкие правила доступа. Обычно наследование бывает публичным (хотя во многих сложных библиотеках по умолчанию наследования, которое не является публичным, и иногда это бывает полезно).

            Другие ЯП отказались от таких изощрённых средств управления доступом.

            Ада: есть понятие тегированной (помеченной) записи:

                        type T is tagged record

                                                           ...

                                               end;

- из помеченной записи можно наследовать:


type T1 is new T with record

                                               ...

                                   end;

новое при наследовании (уточняющая часть)


            Так как в Аде, как и в Обероне, все функции и процедуры ещё не считаются членами класса, то Т1 полностью наследует все операции (операции доступа к самой записи, плюс операции, описанные в том же пакете, что и тип данных Т).

            Приватные ТД:

                        type T is tagged private;

            следовательно внизу должно быть:

                        private

type T is record ... end;

                        end;

            Можно расширять(в том числе и приватныеТД) с помощью конструктора new, после которого стоит ТД. А если хотим изменить только набор операций- специальная конструкция:

                        type T1 is new T

                                   with null record;

            Почему так надо писать? Что никаких членов не добавляем.

                        type T1 is new T;

            Но в чём принципиальная разница этой и предыдущей конструкции? Нарушается первая, самая главная, аксиома концепции уникальности типов (каждый объект данных имеет тип и этот ТД единственный). ООЯП нарушают её тем, что если есть наследование и из Т наследуем Т1, то объекты Т1 одновременно являются и объектами типа Т: Т     Т1 (появляется отношение наследования, которое уместно обозначать, как отношение включения). Но аксиома №4: различные ТД не совместимы по операциям. Следствие:

                        procedure P(x: T);

            её фактический параметр может быть типа объект Т1. И это относится к любой операции, в том числе, и операции присваивания.

            Итак, если Т => Т1 (из Т выводим Т1)

1)                           a       b

            a = b;                                      - корректно

            P(a), P(b) /* тут Т := Т1 */     - можно

но нельзя:

            b = a;

                        procedure P1(x: T1);

                        P1(b);

                        P1(a);  { T1 := T }

            Можно было бы придумать интерпретацию, где это было бы корректно. Как вообще интерпретировать соответствующее присваивание в этом случае?

            b = a;- b не является полностью определённой переменной, следовательно это присваивание (и такая передача параметров) некорректно.

2)В языках, где есть указатели и ссылки, которые являются базовыми ТД, и, вроде бы никакого наследования относительной базовых ТД вводится не может, а наследуются у нас в языках либо классы, либо их аналогии (структуры или записи). Но для ссылок и указателей приняты те же правила. Если P => P1, то мы считаем, что у нас для ссылок и указателей имеет место точно такое же наследование(хотя с точки зрения реализации и указатели и ссылки- это просто адреса памяти) в смысле, что к ним применимы те же самые правила в смысле присваивания:

                        PT => PT1

                        pa        pb

                        pa = pb;           - разрешено

pb = pa;           - запрещено

            Ещё замечание о распределении памяти: то есть каким образом выглядит объект. Речь всегда идёт о некой структуре данных, то есть о последовательность полей. То с точки зрения распределения памяти, у нас, в принципе, есть два способа реализации наследования:

1)Линейное распределение памяти

2)Цепное распределение

            С точки зрения копирования, не важно какое распределение памяти применяется. Но с точки зрения эффективности реализации линейное представление эффективнее цепного. Большинство ЯП: линейное распределение памяти, с некоторыми исключениями: SmallTalk- цепное.

                                   Множественное наследование

            Его нет в SmallTalk, поэтому его нет и во многих других ЯП. Настоящее множественное наследование есть только в языке С++.

            Общий случай наследования в языке С++:

                        class X: список баз { ... };

            Где в элемент списка баз:

                        [модификатор] имя_типа

Перед соответствующим именем типа может стоять ключевое слово virtual.

Что означает множественное наследование?

            class X: public Y; public Z { ...};     

Это простое и очевидное расширение понятия единичного наследования: Так будет выглядеть структура класса Х(часть, относящаяся к классу Y; часть, относящаяся к классу Z; и часть, относящаяся к классу Х):

            Если в языке есть только единичное наследование, то структура будет выглядет как дерево:

            Множественное наследование:

                        void f() {i = 0;}

                        void g() {k = 1;}

            О каком i и о каком k идёт речь (из Y или Z)? Тут может возникнуть конфликт имён. Так как язык С++ единственный язык реализующий множественное наследование по данным, то как в нём решаются эти проблемы?

            С точки зрения функций класса W нет никакого правила, которое говорит, что лучше, и надо явно указать из какого класса это к нам пришло:

                        Y::i;     X::i;     Y::k;    Z::k;

- есть механизм уточнения имён, с помощью него разрешаем конфликт имён. Но он не будет работать в следующем случае:

            В этом случае механизм уточнения имён никак не работает, и поэтому, этот случай запрещён. Нельзя, чтобы в списке баз появлялись одинаковые классы (на разных уровнях могут).

                                   Виртуальное наследование

            Одиночное наследование позволяет моделировать иерархию. А такое наследование позволяет нам моделировать некоторые сети, но не все сети можно смоделировать с помощью такого механизма. Например, бриллиантовое (ромбовидное) наследование:

           

Разница с точки зрения распределения памяти. В первом случае у нас есть только один экземпляр объекта класса Х, а во втором- два. Так же очевидно, что при схеме 1) распределение памяти для Z нелинейное.

            А зачем нам такое может понадобиться. Например, потоки ввода-вывода из стандартной библиотеки STL:

            Это классический случай ромбовидного наследования, и он реализуется следующим образом:

class W: virtual public Y; virtual public Z {}

Неприятная особенность состоит в том, что ключевое слово virtual надо писать не при определении класса, а при наследовании.

Множественно наследование в ограниченном виде поддерживается в ЯП С# и Java. Там есть специальное языковое понятие «interface»- класс без данных: есть только функции члены и статические члены константы (он содержит только описание функций). Допускается наследование одного класса и множественное наследование нескольких интерфейсов.

Java:

            public class X extends Y implements I1,I2,I3 { ... }

            /* implements = реализует */

C#:

            public class X: список имён { ... };

- список имён ::= одно имя класса + имена интерфейсов. Это расширение концепции единичного наследования.

В С++ можно моделировать произвольные схемы наследования. С матиматической точки зрения там у нас есть некая математическая абстракция «решетка классов».

            Во всех остальных ЯП данные можно наследовать только единичным образом.

                                   Наследование и области действия

            Во всех ЯП, за исключением Оберона классы рассматриваются как вложенные области действия, то есть каждый новый класс- это новая область действия.

                        X int i;

                        Y double i;

            Аналогично:

                        { int i;

                                   { double i;       - статически вложенный блок

                                               i           // double

                                   }

                                   i           // int

                        }

            Это статические области действия. Но из вложенного блока нельзя сослаться на i внешнего блока. А в классах можно, используя возможность уточнения: Y -> X::i;. Классы образуют вложенные области действия.

            В  Аде есть управление видимости, а в языке С++ мы говорм об управлении доступом.  В чём отличие?

                        class X {

                                               ...

private:

            void f()

            ...

                        };

                        Y         f();

            В классе Y видимость есть, а доступа нет (для функции f();).

            Если бы речь шла об управлении видимостью, то всё было бы «ОК». А тут логически (здравый смысл)- это обращение к функции f(); из класса Z, но компилятору обе функции из X и Z видимы независимо от модификаторв (private, public). Тут компилятор не знает о какой функции идёт речь. Нет ошибки, только если напишем Z::f();. То же самое распространяется и на замещение имён.

                                                Замещение имён


            Компилятор сообщит об ощибке, так как он сначала пытается разрешить имя, а потом смотрит доступ, не доступ или что ещё, то есть компилятор начинает разбираться с семантикой только после разрешения имени. Общая схема: сначала компилятор должен догадаться, что это за имя, а потом- правомерно или нет обращение к нему. Тут верно только a = X::g();.

            В Обероне закрыть имя запрещено: даже производные классы рассматриваются как единая область действия имён..

А зачем нужно наследование? Его легко промоделировать, что и делается при ОО программировании, в языках, где нет наследования (XTool- надстройка над Xlib: она более ОО, чем какая-нибудь библиотека классов MFC, хотя написана на С}.

            struct X { ... };

            struct Y {

                        struct X x;

                        ...

            };

-это менее удобно, так как обращаемся через имя переменной «х». То есть чисто одно понятие наследования с точки зрения программирования ничего не даёт. Но, когда есть наследование, появляется концепция динамического связывания.

 

Глава 2: динамическое связывание

(методов)

            С++:

                        class X {

                                   public:

                                               void f();

                                   ...

                        };

                        class Y: public X {

                                   public:

void f();          /* у них разные области действия -> нет противоречия */

                        ...

            };

            X x;

            Y y;

            x.f();                // X::f();

            y.f();                // Y::f();

            Аналогично, когда у нас указатели:

            X* px;

            Y* py;

            px->f();           // X::f();

            py->f();           // Y::f();

Тут вложенные области действия. Это статическое связывание. Оно может быть разрешено во время компиляции.

Другая ситуация, если у нас опять появляется ключевое слово virtual только в другом смысле (Страуструб в 86 году боялся вводить новые ключевые слова):

            class X {

                        virtual void f();

            };

Если перед «f» появилось ключевое слово virtual- это означает то, что это функция будет вызываться в зависимости от динамического типа. Для ООЯП появляется ещё понятие «динамического типа». Как мы уже говорили: «каждый объект данных обладает некоторым типом». В традиционных ЯП этот тип только один, а в ООЯП объект данных может обладать, вообще говоря, совокупностью, но, разумеется, конечной совокупностью типов. Тут если объект данных принадлежит типу Т, то он принадлежит и всем типам из которых наследует Т, тоже относиться и к операциям. Как только объект данным появился в памяти, он своего типа не меняет (так сделано в большинстве ЯП). Тот тип, о котором мы говорили раньше, это так называемый «статический тип»- тип, который приписан объекту в момент его объявления, он должен быть единственным. Коль скоро объект размещён в памяти, то он принадлежит одному статическому типу и этот тип не может меняться. Примеры статических типов:

            X x;     // статический тип X

            Y y;     // статический тип Y

Динамический тип: им обладают не все ТД, а именно, указатели и ссылки (у классов его нет):

            X* px;

            Y* py;

            Статический тип px- Х, py- Y, но при присваивании, мы уже начинаем различать статический и динамический типы. Допустимо (так как объектам производных типов можно присваивать объекты базового типа):

                        px = py;

            -теперь px указывает на объектт класса Y. Статический тип px остаётся X. В С указатели должны ссылаться только на объекты соответсвующего типа, следовательно, там нет разницы между динамическим и статическим типом. Как ттолько появилось наследование, то у нас получается, что px может указывать не только на объекты класса Х, но и на объекты любого производного класса. Вот это и есть динамический тип указателя, то есть динамический тип указателя- это тип объекта, на который в данным момент ссылается указатель. Очевидно, он динамический, поскольку при присваивании этот тип может меняться. Тоже самое обобщается и на ссылки, поскольку с точки зрения реализации- это тоже самое.

                        X &c = x;

                        c:         статический тип- Х

                                   динамический тип- Х

                        Х &с = y;

                        c:         статический тип- Х

                                   динамический тип- Y

            Чем отличаются виртуальные функции от невиртуальных? Строго говоря, сами функции- ничем. Отличаются механизмы их вызова, механизмы их связывания. То есть виртуальные функции с точки зрения реализации и то как они компилируются  ничем не отличаются от обычных, невиртуальных функций. Если вызываем функции по объектам:

                        X x;

                        Y y;

                        ...         <что бы здесь ни было написано>

                        x.f();    // X::f

                        y.f();    // Y::f

            - то виртуальные и невиртуальные функции неотличимы. А в случае указателей ситуация совершенно другая (для них вызов функции «f» произойдёт в зависимости от динамического типа:

                        X* px;

                        Y* py;

                        ...

                        px->f();

                        py->f();

            то всё зависит (вызов какой функции будет произведён) от того, что написано тут (какие значения имеют px и py).

1)Если тут

                        px = new X;

                        py = new Y;

, то динамический и статический типы не отличаються, и будут вызваны:

                        X::f

                        Y::f

2)Если тут

                        py = new Y;

                        px = py;

, то будет вызвана функция в зависимости от динамического типа:

                        Y::f

                        Y::f

3)Если тут

                        class Z: public Y {

void f() { ... }

            ...

};

                       

            px = new Y;

            py = new Z;

, то

                        Y::f     , так как динамический тип px- это Y

                        Z::f      , так как динамический тип py- это Z

            Следовательно вызов виртуальной функции происходит в зависимости от динамического типа. Тут речь идёт именно о механизме вызывов. И это (виртуальные функции) применимо только для ссылок и указателей. А почему бы и не сделать, чтобы при «x = y;» динамически менялся тип х? Это можно сделать, но тут придётся динамически перераспределять память.

            Delphi, C#, Java: имя объекта- это ссылка, следовательно, нет разницы, вызываем мы через имя объекта или как-то ещё- у нас речь идёт всегда о вызове через ссылку (в Java вообще нет понятия указателя). Любая видная функция рассматривается как виртуальная. Единственное отличие Delphi от C# и Java состоит в том, что в Delphi есть деление функций на виртуальные и невиртуальные. В C# и Java все функции считаются виртуальными (то есть динамически связываемыми).

            С++ отличается от Delphi. Если на каком-то уровне иерархии определили функцию как virtual, то все её наследники тоже являются virtual, автоматически (и в наследниках можно явно не указывать). А в Delphi допускается, чтобы функция была невиртуальной.

            Механизм виртуального замещения отличается от механизма перегрузки операций: во-первых виртуальные функции должны иметь один и тот же прототип (а в старом варианте С++ даже один и тот же тип возвращаемого значения, а в новом стандарте оно может быть производным типом от типа которым обладает самый первый прототип). Это уже не перекрытие операций (которое как раз и основано на различие прототипов, где компилятор разрешает какой вызов производить), а замещение:

                        overridding     -           динамическое замещение

                        overloading     -           статическое перекрытие

            Delphi:

                        T = class

                                   procedure P: virtual;

                        end;

                        T1 = class (T)

                                   procedure P; override             - виртуальная, как в С++

                        end;

            Она не является виртуальной, а подменяет собой виртуальную функцию, если нет override, и, более того, тоже будет если мы просто напишем у неё ключевое слово virtual.

            Тоже самое и в языке С#:

                        public class X {

                                   public virtual void f() { ... };

                                   public void f(int i) { ... };

Лекция "Лекция 18 - Анализ вынужденных процессов" также может быть Вам полезна.

                        };

                        отличаются по прототипам

                        public class Y: X {

                                   public override void f();

                        };

            И если в классе Y просто напишем void f, то никакого замещения производиться не будет.

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