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

Лекция 20

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

Лекция № 20

  Объекты абстрактного класса нельзя заводить в памяти. На объекты этого класса можно ссылаться с помощью указателей и ссылок, при этом реально они будут указывать на один из объектов произвольных классов, которые  часто называют конкретными классами. Концепция абстрактного класса оказалась очень удобной с точки зрения ООП, поскольку идея объектно-ориентированного дизайна состоит в том, что все свойства, которые нам нужны, мы формируем в виде функционального интерфейса, и пользователи работают с объектами соответствующего типа данных только через этот функциональный интерфейс. Абстрактный класс предоставляет такой функциональный интерфейс, а конкретизироваться он будет уже в конкретных классах. Это позволяет достаточно гибкое расширение, поскольку пользователь, работая с указателями и ссылками на абстрактные классы, работает только с наиболее общими свойствами. Поскольку эта концепция важна, она естественным образом вошла в другие языки программирования. И в языке Java, и в языке C#, и в языке Delphi есть специальный спецификатор abstract. В C#, Java

   public abstract class X{

    public abstract void f( );

   }

Если класс абстрактный, то впереди обязательно должен стоять спецификатор absract. Кроме того это ключевое слово должно стоять перед каждой функцией, которая не реализована. Точно также как и в С++ эти объекты не могут размещаться с помощью new (другого способа размещения объектов в этих языках нет).  Синтаксис Delphi очень схож с этим. Главное отличие в том, что в Delphi можно порождать объекты абстрактных классов, при этом компилятор выдает предупреждение. В любом случае, при попытке обратиться к абстрактному методу будет порождаться исключение, которое теоретически можно ловить обработчиками исключений. Абстрактный класс- это класс, в котором есть хотя бы одна абстрактная функция. Класс, в котором все функции являются чистыми виртуальными, и в которых, как следствие, отсутствуют данные. В языке С++ для такого класса будет одна неабстрактная функция - конструктор. Конструктор, в отличии от деструктора, не может быть виртуальным. Полностью абстрактный класс, или интерфейс (этот термин можно видеть даже в книгах по С++, хотя в этом языке понятие интерфейса нет) – класс, в котором все функции являются чистыми виртуальными функциями. Это класс без данных. В памяти допустима следующая реализация такого класса (класс состоит из одного указателя на ТВМ)

         ссылка на ТВМ

                                       

Пример: реализация множеств.

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

   class Set{

    virtual void Incl(T & x) = 0;

    virtual void Excl(T & x) = 0;

    …

    virtual bool IsIn(T & x) = 0;

    …

   }

  В языке ФОРТРАН и его производных множества были. В современных языках встроенный базисный тип данных множество отсутствует.  Понятие множества можно реализовывать совершенно по-разному. В реализациях Паскаля и его наследников (Модула-2, Оберон) понятие множество присутствует только как битовая шкала. Это одна из возможных реализаций множества, и она хороша, например для того, чтобы можно было напрямую и достаточно элегантным способом адресовать бит внутри машинного слова. Другой вариант множества – это действительно некоторое множество, контейнер каких-то объектов. Один из вариантов не зависеть от реализации множества вообще – чтобы пользователь просто работал с множеством, а как оно реализовано – не важно. Set – абстрактный класс. Пользователю необходимо знать только его. Различные реализации из одного класса Set могут получаться только с помощью множественных наследований. Разные контейнеры могут соответствовать разным реализациям. Например

   class Slist_Set: public Slist, public Set{

    …

   };

Slist – класс, который реализует однонаправленный список. Класс Slist_Set должен содержать реализации всех абстрактных методов. Другой пример

   сlass Seek_Set: public BitScale, public Set{

    …

   };

BitScale – класс, который эффективно реализовывает битовую шкалу. Можно еще все это реализовать с помощью шаблонов, чтобы не зависеть от типа данных Т. При порождении нового пакета, мы обращаемся к конструктору нужного нам класса, но пользователь всегда работает с помощью Set. Например, у нас может быть некоторая статическая функция

   Set * MakeSet( ){

    return new Slist_Set;

   };

Для того, чтобы сменить реализацию соответствующего класса, необходимо сменить только реализацию функции MakeSet. То, что и интерфейс, и реализация класса находятся в одном месте приводит к тому, что при изменении структуры класса с точки зрения реализации, приводит к перекомпиляции всех клиентов, которые используют этот тип данных. В нашем случае клиентам необходимо знать только заголовочный файл, в котором находится интерфейс класса Set. В результате, чтобы изменить реализацию нам нужно изменить реализацию одной функции, а интерфейс всегда будет один и тот же. И это не вызывает перетрансляции клиентов. Это очень интересное свойство динамического связывания и виртуальных функций. Понятие интерфейса – это как раз то понятие, которое связывает воедино концепцию абстрактного типа данных и концепцию наследования. Все, что мы сказали об абстрактных типах данных, естественно, относится и интерфейсам. В более молодые, чем С++, языки программирования понятие интерфейса вошло явным образом. В частности и в языке C#, и в языке Java оно присутствует. Проблемы, которые возникают при множественном наследовании в языке С++, связанные с механизмом реализации динамического связывания виртуальных методов, в случае этих языков не возникают. Например

   public interface Set{

    void Incl(object o);

    void Excl(object o);

    …

   }

У интерфейса по определению все функции являются публичными. Нет смысла упрятывать от кого-то эти функции, т.к. интерфейс – это то, что мы экспортируем для того, чтобы пользователь мог работать с конкретной реализацией класса через абстрактный интерфейс. Поэтому, естественно, по умолчанию все члены интерфейса считаются публичными. В языке Java интерфейс кроме объявлений функций-членов (которые, естественно, будут абстрактными => без реализации) также может содержать набор констант, т.е. статических финальных переменных.

   public interface Set{

    void Incl(object o);

    void Excl(object o);

    static final int NUL = 0;

    …

   }

final в языке Java говорит о том, что это константа, а static – что она не является членом соответствующего класса. В языке C# вместо статических констант (они там особо не нужны) выступают перечислимые типы данных, которые в Java отсутствуют. Т.е. в C# интерфейс кроме объявления функций может также содержать объявления перечислимых типов. Больше ничего в интерфейсе не допускается. Интерфейс нужен для того, чтобы описывать свойства различных конкретных классов с абстрактной точки зрения. Поскольку в нем нет данных, интерфейс выступает как ссылка на ТВМ. Если к нему теперь добавить еще указатель на сам объект, то через интерфейс можно работать с конкретным объектом. Например, с точки зрения реализации в этих языках интерфейс может выступать как пара указателей. Первый указатель – указатель на ТВМ. Второй указатель – указатель на сам объект. Либо он может выступать как некоторая пара: указатель на объект, вначале которого всегда находится ссылка на полную таблицу виртуальных методов, + смещение d в соответствующей таблице виртуальных методов. d показывает на какую именно часть ТВМ мы должны ссылаться.

          d         

                              полная ТВМ             


                                Y      d

                                I1  

                                I2  

                               …               

          

Очень простая реализация.

В Java наследование выглядит следующим образом

   class X extends Y implements I1, I2, …{ …};

(class X extends Y {…}; - обычное одиночное наследование (extends – расширяет).) I1, I2, … - имена интерфейсов. Очевидно, можно заводить переменные типа интерфейс, а им будут присваиваться объекты классов, которые реализуют этот интерфейс. Например

   I1 i = new X;

при  условии, конечно, что Х не является абстрактным типом.

Если класс описан таким образом, но не реализует хотя бы один из методов интерфейса, он, очевидно, будет абстрактным. Класс Х будет конкретным только в том случае, если он реализует все функции данных интерфейсов.

Не возникает никаких проблем с реализацией за счет того, что наследование по данным единичное (члены-данные класса Х – это члены-данные класса Y).

  В языке C# синтаксис немного другой

   class X: Y, I1, I2, … {…}

В списке Y, I1, I2, … только одно имя может быть не интерфейсом. В C#, как и Java всегда ведется наследование от типа Object. Поэтому, если в этом списке нет ни одного класса, а все интерфейсы, то будет подставлен тип Objеct. Понятие интерфейса достаточно гибкое. Абстрактный класс, у которого все функции абстрактные, отличается от интерфейса тем, что класс – это, в лучшем случае, уже какая-то конкретная реализация какого-то понятия. Например, Set – это интерфейс, в отличии от Slist. Что имеет смысл выделять в интерфейс? Часто бывает, что объект достаточно многолик. Обычно смотрят на то, какого рода свойства должен реализовывать объект. Чаще всего с помощью интерфейса мы выражаем свойства, которые, может быть, не специфичны для данного объекта. Например, в языке Java есть стандартный интерфейс, который называется Cloneable. Именно в этот интерфейс входит функция

   object Clone( );

Так решена проблема копирования в языке Java. Поскольку это ссылочный язык, то, естественным образом, при копировании объекта копируется только ссылка – так называемое поверхностное копирование. Если нужно глубокое копирование, надо вызывать метод Clone. Метод Clone в типе данных Object. Свойство копироваться – это не специфичное свойство какого-то конкретного класса, это свойство всех объектов вообще. Поэтому вполне резонно, что класс Object описан таким образом

   class Object implements Cloneable …

Он, в частности, реализует среди прочих и интерфейс Cloneable. Это не единственный интерфейс, который реализует класс Object. Понятие интерфейса позволяет нам реализовать понятие рефлексии. Рефлексия – механизм языка, который позволяет из программы управлять поведением компилятора и среды времени управления. Например, в языке C# есть такая интересная конструкция

   foreach (T i in S){…}

Контейнер – это, в частности, массив – встроенный тип данных языка C#.  Тогда мы можем написать для массива такой цикл. Переход от предыдущего к следующему элементу в таком цикле реализуется эффективней чем непосредственная адресация a[i]. Это конструкция языка. Как в языке C#, так и в языке Delphi есть понятие индексера. Если класс является контейнером, т.е. содержит в себе данные других типов данных,

  Cont A = new Cont;

 то, в принципе, к такому контейнеру можно обращаться двояко: можно работать с ним как с целым

   A = B;

и в то же время его можно индексировать

   A[i]

В языке C#, как и в языке Delphi  нет понятия переопределения стандартных знаков операций. В C++ можно переопределить операцию индексирования. Если в C# описана функция

   this(T x){…}

Эта функция и называется индексером. У нее должен быть один параметр. Ее параметром выступает индексом. Например, мы можем индексировать по целому типу, по строковому типу (тогда это типичный пример ассоциативного контейнера).Возвращает какой-то элемент соответствующего типа. Можно перекрывать различные типы индексов. В результате мы можем писать

   A[i]

Таким образом можно превращать контейнер в структуру, которая поддерживается языком. В интерфейсе IGetEnumeration есть метод

   IEnumerator * GetEnumerator( );

Если реализуется такой интерфейс, то для этого класса можно вызвать метод GetEnumerator, который возвращает интерфейс IEnumerator. В интерфейсе IEnumerator есть методы типа

   MoveNext

   Current

   …

Если компилятор в таком цикле видит объект класса, который поддерживает IGetEnumeration, то он делает цикл по всем элементам.

   enum = S.GetEnumerator( );

поскольку S реализует соответствующий интерфейс. Далее enum применяется для того, чтобы ссылаться

   i = enum.Current( );

в конце цикла делаем

   enum.MoveNext( );

  Реализация ряда интерфейсов приводит к тому, что компилятор начинает работать специфическим образом. Это типичный случай рефлексии, когда мы поведением компилятора управляем через конструкции языка.

  Понятие интерфейса распространено достаточно широко. Такие модели распределенных объектов как COM Microsoft и CORBA в явном виде используют понятие интерфейса. Есть специальный язык IDL (Interface Description Language). COM иCORBA – достаточно разные реализации технологии распределенных объектов (т.е. объектов, которые могут общаться между собой по сети). Тем не менее что-то общее в этих моделях есть. Прежде всего то, что поведение объектов и в рамках COM, и в рамках CORBA определяется в терминах некоторого языка описания интерфейсов. Предложение IDL, в частности, - это и есть описание того, какие методы и какие свойства есть у того или иного объекта. Описание интерфейса не зависит от языка программирования. Уже потом эти специальные компиляторы с интерфейсного языка генерируют привязки этих объектов, во-первых, к конкретным языкам программирования, и, во-вторых, к конкретному транспортному механизму. Существенно, что раз у нас есть такого рода модели, то мы можем, в результате перейти на более языково не зависимый уровень. Интерфейсы и на Java, и на C#, и на Delphi можем описать с помощью IDL. Механизм реализации в COM и CORBA существенно различаются. Модель CORBA, с одной стороны, богаче чем COM. Она более объектно-ориентированная. Если в COM нет наследования, а есть чистая агрегация интерфейсов, то в CORBA есть и наследование и агрегация. Сейчас наблюдается тенденция перехода от CORBA, COM к платформе .NET.

  С точки зрения реализации множественное наследование от интерфейсов не представляет сложности. Но при множественном наследовании остается проблема одинаковых имен. Каждая библиотека представляет весьма богатый набор интерфейсов. Поэтому довольно часто происходит пересечение в интерфейсах по именам. Когда мы сами разрабатываем интерфейс, это не так страшно - мы можем изменить соответствующее имя. В C# есть такая возможность как преимущественная реализация интерфейсов. Представим себе ситуацию

   class X: I1, I2 {…}

   interface I1{

    void Execute( );

     …

   }

   interface I2{

    void Execute( );

    …

   }

   X x;

   x.Execute( );

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

   void I1.Execute( ){…}

   void I2.Execute( ){…}

Теперь для выбора соответствующего метода

   ((I1)x).Execute( );

   ((I2)x).Execute( );

  В языке Java в таком случае предлагается выстроить нужную иерархию.

   class XX implements I1{

    void Execute( );

   };

   class X extends XX implements I2{…};

Тут уже есть некоторая преференциальность.

   x.Execute( );

означает вызов Execute для класса Х, и, значит, для интерфейса I2. Преобразование типа

   XX(x).Execute( );

соответствует реализации Execute для интерфейса I1.

    Глава 4. Динамическая идентификация типов.

  RTTI – Run Time Type Identification (для ссылочных объектов). Рассмотрим абстрактный базовый класс Figure, у которого есть метод Draw( ), объявленный как чистая виртуальная функция для языка C++ (или соответственно абстрактная для других языков программирования). У нас есть некоторая коллекция объектов Figure. Метод DrawAll( ) пробегает по всей коллекции и для каждого элемента коллекции вызывает метод Draw. Поскольку коллекция состоит из объектов типа Figure, то для них определен метод Draw. Проблема в том, как писать контейнер. Лучше пользоваться стандартными контейнерами. В языке C++ есть такая мощная вещь как шаблоны, и мы на основе контейнера делаем специализацию типа контейнера классом <Figure>. Поэтому в языке С++ вопросу о динамической идентификации типа особого внимания не уделяется. Если в языке нет понятия шаблона можно писать свой контейнер для каждого типа данных. Все коллекции языка Java, все коллекции языка Delphi, все коллекции языка C# содержат объекты типа Object. Механизм шаблонов дает нам гарантию при компиляции, что неправильный объект (объект не типа Figure) не может попасть в коллекцию. Если коллекция просто над типом данных Object мы можем неправильно занести в нее объект. Для того, чтобы безопасно работать с коллекциями такого типа необходим механизм динамической идентификации типа. Т.е. если объект х не является объектом производного типа от класса Figure, то мы генерируем сообщение об ошибке. Нам нужен специальный механизм, который позволяет узнать принадлежит ли объект соответствующему типу. Даже в библиотеке языка С++ иногда возникает потребность в динамической идентификации типа. Такой механизм есть во всех ООЯП. Минимальный случай динамической идентификации типа мы видим в языке Оберон. Там есть 3 конструкции:

1) Проверка типа

2) Страж типа

3) Групповой страж – модификация стажа типа.

Т<T1 в случае, если тип Т1 прямо или косвенно выводится из типа Т. Проверка типа – это логическое выражение.

   t is T

где t – объект с динамическим типом, например, ссылка или указатель. Т – тип. Пусть Т1 – статический тип t. Только в случае, если T1<T, имеет смысл делать проверку. Если динамический тип t >= T, то проверка есть истина, в противном случае – ложь. Если Т1>=Т то t по правилам наследования является одновременно объектом типа Т и делать проверку смысла нет. В случае, если Т и Т1 находятся на разных ветвях иерархии, t не может быть типа Т. Если требуется проверка, компилятор поставляет код, который сравнивает динамический тип t и Т. У нас есть вторая конструкция – страж. Она имеет вид

   t(T)

t – объект данных, Т – тип. Опять t – это либо ссылка, либо указатель (т.е. имеет динамический тип).

   if t is T then

    t ~ T;

   type X = record

                  i:integer;

                  end;

   type Y = record

                  j:integer;

                  end;

   procedure P(VAR T: X);

    …

 Внутри процедуры мы можем писать T.i, но не имеем право писать T.j. Но мы можем писать T(Y).j. В этом месте компилятор выполняет проверку типа, и, если Т действительно является ссылкой на объект типа Y, то он разрешает операцию, если – нет, то он генерирует исключительную ситуацию. Это похоже на контролируемые преобразования указателей. В нашем примере эта операция пройдет.

  Программа на Обероне чаще всего имеет такой вид

   if t is T then t(T). …

    else

     …

   endif

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

   with t:T do

    …

   End

В нашем случае можно писать так

   with T:Y do

    T.i

    T.j

   end

   Групповой страж работает так: проверка типа выполняется один раз. Поэтому соответствующая программа более эффективно может быть переписана в виде

   if T is Y then

    with T:Y do

     …

   else

    …

   endif

При этом все равно выполняется 2 проверки типа. Нет никаких неконтролируемых преобразований. Это значит у нас нет возможности неправильно обратиться.

  Этот минимальный набор существует практически во всех языках.

  В С++ есть специальный класс, который называется typeinfo, в котором переопределен метод сравнения ==. Он содержит в себе некоторую информацию о типе. Например, имя соответствующего типа и какую-то дополнительную информацию. Есть специальная псевдофункция typeof(e), где е –выражение динамического типа, которая возвращает ссылку на typeinfo. Проверка

   t is T

не эквивалентна

   typeof(t)==typeof(T)

т.к. t is T истинна, если динамический тип t >=T, а во втором случае сравнение только на равенство. Поэтому typeof используется достаточно редко и несет в себе не слишком много информации. В языке Оберон проверка типа t is T нужна для того, чтобы можно было безопасно обращаться со стражами типа. Страж типа похож на контролируемое преобразование указателей. Поэтому динамическая идентификация типа в языке С++ осуществляется с помощью контролируемого динамического преобразования указателей. Есть специальная конструкция

   dinamic_cast<T>(e)

Т – некоторый тип данных (ссылочный или указательный), е - выражение. Если Т1 – статический тип е, если  T не сопоставимо с T1, тогда компилятор выдает сообщение об ошибке, если Т<=T1, то тут и преобразовывать нечего. dinamic_cast очень похож на страж типа, но он одновременно работает и как страж типа, и как проверка типа. Если Т – указательный тип данных, то в случае, если динамическая проверка типа истинна, выдается соответствующий указатель, преобразованный к типу данных Т, иначе NULL. Если Т – ссылочный тип, то в случае, если динамическая проверка типа истинна, выдается ссылка на Т, иначе генерируется исключительная ситуация, поскольку в С++ нет нулевой ссылки. Понятия группового стража нет. Оно и не нужно, поскольку указатель сразу преобразуется. В современном стандарте языка С++ не рекомендуется использовать старые формы преобразований. По аналогии с dinamic_cast было введено еще 2 преобразования.

   static_cast<T>(e) 

   reinterpret_cast<T>(e)

static_cast как и dinamic_cast применяется в случае, если Т – это ссылочный или указательный тип данных, совместимый с типом е. Только static_cast в отличие от dinamic_cast выполняет неконтролируемое преобразование.

   X * p; Y * p1;

   p = (X *) p1;                      

- это то же самое, что

   p = static_cast<X *>(p1);

Как правило это преобразование от базового типа к производному (но может быть и наоборот).

  reinterpret_cast используется, когда типы Т и е не совместимы. Не выполняет никакого контроля.

  По настоящему динамический тип будут иметь только те классы, в которых есть виртуальный метод. Преобразование типов dinamic_cast допустимо только для классов с виртуальным методом.

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

   ((T1)x).Execute( );

в этом месте компилятор вставляет проверку, что х действительно реализует данный метод.

  Иногда полезно просто узнать, принадлежит ли объект данному типу. В языке Delphi есть ключевое слово is.

   e is T

  В языке Java вместо ключевого слова is ключевое слово instanceof.

  В языке Delphi есть еще оператор присоединения, который ведет себя точно также как групповой страж типа. (В Delphi не различаются маленькие и большие буквы)

   with t as T1 do

    begin

     …

Рекомендуем посмотреть лекцию "Оглавление".

    end

Происходит динамическая проверка типа, и, если она не проходит, выдается сообщение об ошибке. Нам не нужно писать все время T1(t). Можно писать нечто типа

   e:= t as T1;       - аналог dinamic_cast из С++

   e:= T1(t);          - аналог static_cast из С++

Т.е. на Delphi тоже есть возможность программировать чуть более эффективно.

  Delphi, Java, C# обладают достаточно мощным дополнительным средством  идентификации типа, поскольку у них есть базисный класс TObject или Object. Он содержит в себе много методов выдачи информации о типе. Например, у типа Object обязательно есть метод, который выдает название типа, и может быть метод, который позволяет более гибко динамически его идентифицировать.

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