Skip to content

Instantly share code, notes, and snippets.

@NickTikhomirov
Last active March 21, 2023 20:13
Show Gist options
  • Save NickTikhomirov/29cc8ef16301496d1a4187635159565f to your computer and use it in GitHub Desktop.
Save NickTikhomirov/29cc8ef16301496d1a4187635159565f to your computer and use it in GitHub Desktop.
questions for answers

UPD: РЕЖИМ БЕЗУДЕРЖНОГО СПИСЫВАНИЯ (удобного поиска). Теперь вопросы тематически раскиданы по разным гистам.

Ссылки:

========

Основы (1-2 и 4-5)

Потоки (27-31)

Остальное (6-8 и 33-34)

====================================

Задачи (От Никиты Першаева и Жени)

Вопросы (Сайт Быкова)

====================================

Внимание! Этот гист больше не будет обновляться! Все правки будут вноситься в тематические гисты.

Автор билетов 14-34: Толик
Автор билетов 1-13 и крутого дезигна: Никита

__________________________________________________________________

Билет 1. Классы в языке С++: объявление, поля и методы классов. Определение функций внутри класса и за пределами класса, примеры.

Класс - это структура, в которую в языке С++ введены методы для обработки полей.

Ключевые слова для объявления класса:

  • struct
  • class
  • union //Не самое полезное слово, но в лекциях у Быкова было
Add: аффтар только что сползал создал union, и оно приняло метод. Всё работает, union тоже умеет в класс.
Add2: а, нет, наследовать что-то не хочет. Не сильно-то и умеет.

Образец объявления:

<ключевое слово> <имя класа>{
  <компоненты>
};

В качестве ключевого слова может использоваться как struct, так и class. Разницы никакой кроме того, что у class все поля по умолчанию имеют статус доступа private, а у struct (и union) - по умолчанию public. Это единственная разница между классом и структурой.

Компоненты класса: поля и методы.

Инициализация полей происходит раньше конструктора.

Модификатор const на методе (в конце заголовка метода) запрещает ему обращаться к полям класса с целью изменений.

Модификатор mutable на поле класса (перед полем) позволяет методам с модификатором "const" изменять его.

Методы внутри класса и вне его.

Пример объвления метода внутри класса. Этот метод будет, по возможности, подставляемым - его текст компуктор будет просто брать и пихать в то место, где происходит вызов

class a{
  int answer(){
    return 42;
  }
};

Пример объвления метода вне класса. В его случае всё будет как обычно - передача управления на функцию, все дела.

class a{
  int answer();
};

int a::answer(){
    return 42;
}

__________________________________________________________________

Билет 2. Статусы доступа компонент класса. Операции для доступа к компонентам класса, примеры.

Свойство доступности определяет возможность доступа к полям и методам за пределами класса (через имя объекта или через указатель на объект или в производном классе).

Ключевые слова для объявления класса:

  • public -- Свободный доступ для методов, потомков и вызовов извне
  • protected -- Только для методов и потомков
  • private -- Только для методов

Образец объявления:

<ключевое слово> <имя класа>{
  <компоненты1>          // По умолчанию у класса private, у структуры и объединения - public
public:
  <компоненты2>
private:
  <компоненты3>
protected:
  <компоненты4>
public:                  // Можно повторять статусы доступа
  <компоненты5>
};

Операции доступа:

"Сеттеры" и "геттеры" (жарг.), от слов set и get. Не являются какими-то особыми фичами или ключевыми словами, а представляют собой скорее определённый стиль программирования и аттрибут "хорошего кода". То есть считается хорошим тоном сделать поля protected или private, а затем для каждого поля написать setX() и getX(), где X - название поля. Практическая польза - в этих функциях могут быть представлены какие-то проверки или преобразования для значений, чтобы, например, в поле класса не поставили неправильное число. "Геттеры" чаще всего делают с модификатором const.

__________________________________________________________________

Билет 3. Конструкторы и деструктор класса, конструктор копирования и конструктор перемещения, назначение, пример.

Конструктор - блок инструкций, вызываемый при создании объекта класса.

Деструктор - блок инструкций, вызываемый при уничтожении объекта класса.

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

Назначение конструктора: присвоение каких-то значений полям, выделение памяти, открытие файлов и установление сетевых соединений.
Назначение деструктора: соответственно, наоборот очистка памяти и сохранение файлов.

Параметры конструктора. Конструктор может принимать какие-то значения в качестве аргументов - например, для установки значений полям. Также возможно создание нескольких конструкторов с разными параметрами. Деструктор всегда один и без параметров.

Оформление конструктора (внутри класса):

Имя_класса(Список_формальных_параметров){
  Операторы_тела конструктора
}

Оформление деструктора (внутри класса):

~Имя_класса(){
  Операторы_тела деструктора
}

Пример вызова конструктора:

  A *pA1 = new A;           //Без параметров, в куче
  A *pA2 = new A(42,69);    //С двумя параметрами, в куче
  A pA3();                  //С одним параметром
  A pA4 = 1;                //Такая формулировка возможна, если у класса есть конструктор с одним параметром. 
                            //В этом случае эта строчка идентична предыдущей.

Чтобы исключить вызов конструктора через присваивание, как показано в последнем примере, к конструктору дописывается ключевое слово explicit (перед конструктором).

Для конструктора, конструкторов копирования и перемещения, а также деструктора справедливо следующее: если программист не написал их сам, то компилятор допишет их за него. Такие конструкторы(деструкторы) называются неявными. Также важно понимать, что конструктор, декструтор, к.копирования и к.перемещения должны иметь доступ модификатор доступа public.

Конструктор копирования.

Это конструктор вида (для случая внутри класса):

A(A& a){               //A - имя класса
  Операторы_тела конструктора
}

Он обязательно принимает в качестве аргумента ссылку на другой объект того же класса. Если программист пожелает, то конструктор может начать принимать что-то ещё.

Назначение конструктора копирования: копирование всех полей одного объекта в другой (по умолчанию). При переопределении может вытворять всё, что пожелает программист. Например, менять поля местами.

Благодаря этому конструктору, мы можем, например, писать вот так:

  A a1;
  A a2(a1);           //Конструктор копирования
  A a3=a1;            //Равносильно предыдущей строке

В данном примере для a2 и a3 будет вызван конструктор копирования, который скопирует каждое поле a1.

Самое главное про конструктор копирования: если вы оставили для полей-указателей простое копирование, то, когда в первом объекте (который мы копировали) они будут почищены (например, деструктором), во втором они также почистятся, а этого вы, скорее всего, не задумываете.

Если мы захотим сломать конструктор копирования, указав явно только один его вариант, который принимает не только ссылку, но и другие аргументы, то программа сама додумает за нас самый просто конструктор копирования с единственным аргументом (ссылкой). И пример, приведённый выше, всё ещё будет работать. А вот если мы сами напишем конструктор копирования с одним аргументом (ссылкой) и добавим к нему слово explicit, то пример работать перестанет, так как из-за третьей строчки программа не скомпилируется.

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

  A b;
  A a1 = b;          //Конструктор копирования (не explicit)
  A a2;              //Обычный конструктор
  a2 = b;            //Присваивание

Если мы переопределили конструктор копирования, чтобы он, например, менял местами поля и дописывал всякие нехорошие слова к полям-строкам, а затем запустили пример, приведённый выше, то эти действия произведутся только для a1, а объект a2 будет идентичен объекту b.

Конструктор перемещения.

Это конструктор вида (для случая внутри класса):

A(A&& a){               //A - имя класса
  Операторы_тела конструктора
}
Назначение конструктора перемещения: взять поля в исходном объекте и перекинуть в новый так, чтобы исходный можно было свободно уничтожить, не повредив новый. Пример реализации - скопировать поля в новый и занулить указатели в исходном, чтобы его деструктор не добрался до важных объектов.

Вызов конструктора перемещения в коде программы:

  A a1;
  A a2 = std::move(a1);

__________________________________________________________________

Билет 4. Статические компоненты класса, назначение, пример.

Перед полем или методом можно дописать ключевое слово static - тогда он(о) станет станет статическим. На статические поля и методы распространяются модификаторы доступа.

Статическое поле существует в единственном числе вне зависимости от количества объектов класса. Для всех них оно одинаково. Получить к нему доступ можно либо через имя класса (A::a), либо через имя какого-нибудь объекта этого класса. Инициализировать статическое поле надо вне класса, потому что иначе будут происходить всякие странности вроде переинициализации поля с каждым созданным объектом.

Статический метод - такой метод, который может быть вызван по имени класса, даже если не создано ни одного объекта этого класса. Он не взаимодействует ни с какими полями и методами класса кроме статических. К нем можно обращаться точно так же, как к ст.полю.

__________________________________________________________________

Билет 5. Указатель this, примеры использования.

Из курса алгоритмических языков мы знаем, что функции хранятся в стеке, а ещё при вызове не-inline функции у нас происходит передача управления на функцию. Также мы знаем, что методы класса взаимодействуют с полями того объекта, для которого они вызваны. То есть мой геттер фамилии возвращает именно мою фамилию, а не, например, Толину. В связи с этим возникает вопрос - у нас что, все методы класса дублируются столько раз, сколько объектов мы создадим? Прям за каждый объект в стек функций докладывается ещё одна?

Ответ: нет. Просто у каждого (не static) метода есть секретный аргумент - указатель на конкретный объект. Когда мы вызываем метод для какого-то объекта, в него передаётся указатель на него - так функция понимает, с чьими конкретно полями она работает. Программисту тоже разрешено взаимодействовать с этим указателем через ключевое слово this, которое по умолчанию определено внутри каждого (не static) метода любого класса.

Зачем его можно использовать?

0) Например, мы хотим, чтобы метод возвращал нам указатель на этот самый объект => "return this;"
Или у нас есть vector указателей на объекты, мы его по ссылке передаём в метод, и объект кладёт свой указатель 
в этот вектор, только это говнокод.
1) Если у нас какой-то из аргументов метода зовут так же, как и какое-то поле класса. Не понятно, правда, зачем вы 
так его обозвали, но, если такое произошло, то указатель this тоже приходит на помощь. 
Итак, допустим у вас аргумент называется name и поле называется name. Если вы в тексте метода напишете слово name 
(чтобы что-то с ним сложить или присвоить или проверить...), то программа решит, что вы подразумеваете тот name, 
который аргумент. А к name, который поле, обращаемся как раз по указателю this. Например, "this->name=name;". Эта 
строчка положит name-аргумент в name-поле. И ещё раз, это не я странно объясняю, это вы хреново аргументы называете.
х) Самое странное применение this: если у вас плохая память, и вы позабывали названия полей и методов или у вас про-
сто огромная матрёшка из наследований, половину из которых сделалали даже не вы (привет, qt!), то можно написать 
"this->", и любая нормальная среда разработки покажет вам список того, что можно написать дальше (т.е. список мето-
дов и полей), 
НО ЭТО, ОЧЕВИДНО, УЖЕ НЕ ПРО ЯЗЫК С++, А ПРО ВЫБОР СРЕДЫ РАЗРАБОТКИ. 
КАК ОСНОВНОЕ ДОСТОИНСТВО УКАЗАТЕЛЯ this БЫКОВУ ЭТО НЕ НАЗЫВАЙТЕ.

__________________________________________________________________

Билет 6. Указатели на компоненты класса (на функции и поля класса), назначение, пример.

Чтобы обратиться не к компоненту класса, а сразу к указателю на него используются операторы ".*" и "->*". Перед использованием этих операторов сами указатели надо ещё настроить.

Указатель на поле.

Шаг 1: Объявить указатель на поле: [тип] [класс]::*[имя_указателя]

Шаг 2: Настроить его на какое-то поле (сказать ему, на кого он показывает): [имя_указателя] = &[класс]::[поле]

Шаг 3: Использовать [объект класса].*[имя_указателя]

Шаг 3(другой): Тоже использовать [указатель на объект класса]->*[имя_указателя (на поле)]

Обратите внимание, что на шагах 1-2 используется имя всего класса, а на 3-3(др.) используется имя одного конкретного объекта или указателя на него

Пример:

  class point{
  public:
    int x,y;
  };
  
  
  int main(){
    int point::*p = &point::x;     //Александр Юрьевич объединил шаг 1 и 2. Ну, бог ему судья
    point p1;                      //Просто создаём объект
    p1.*p = 5;                     //Уже третий шаг? Быстро же Вы, Александр Юрьевич
    p = &point::y;            //Снова второй? А вы шалун)))) По теме: здесь меняем указатель на другое поле
    p1.*p = 10;                    //И снова третий. В этот раз изменения коснутся поля y.
  }

Указатель на метод.

Шаг 1: Объявить указатель на метод: [тип_возвр] ([класс]::*[имя_указателя])([параметры_метода])

Шаг 2: Настроить его на какое-то поле (сказать ему, на кого он показывает): [имя_указателя] = &[класс]::[метод]

Шаг 3: Использовать ([объект класса].*[имя_указателя])() //другой шаг 3 додумайте сами

Обратите внимание, (...) //то же самое
Обратите внимание2, по сравнению с предыдущим случаем, на первом и третьем шаге появляются скобки и при функции, и на всей конструкции.

Пример:

  class point{
  public:
    int x,y;
    int getX(){ return x;}
    int getY(){ return y;}
  };
  
  
  int main(){
    int (point::*pF)()       //Шаг 1
    pF = &point::getX();     //Шаг 2
    point p1;                //Просто создаём объект
    cout << (p1.*pF)();      //Шаг 3
    pF = &point::getY();     //Шаг 2 (меняем метод)
    cout << (p1.*pF)();      //Шаг 3 (сработает для другого метода)
  }

__________________________________________________________________

Билет 7. Дружественные функции и классы, назначение, пример

"Другом" класса называется функция или другой класс, НЕ ЯВЛЯЮЩИЕСЯ его методами(полями), но имеющие доступ к его приватным полям. Описать друга в классе легко - напишите его заголовок в классе с модификатором friend (в начале). Модификаторы доступа на друзей не влияют.

О друзьях-функциях.

  0) Не являются методами, и к классу имеют доступ, если мы 
    а) Его передали в аргументы (если не по ссылке/указателю, то поменять ничего не может :) )
    б) Создали в самой функкции
  1) В силу этого не имеют this
  2) Могут быть методами других классов
  3) Могут дружить с несколькими разными классами

О друзьях-классах.

  0) Все методы друга могут иметь доступ к полям исходного
  1) Метод - это функция, поэтому 
         #include "О друзьях-функциях"

Пример:

  class A{
    int a;
    int b;
    friend int get_a(A);   //Говорят, по умолчанию для кого-то там private, но на друзей эти фокусы не работают
    friend class B;        //Ставь класс, если тоже не понимаешь, почему Никита до сих пор этим занимается
  //friend idi::poesh(A);  //Дружественная функция, которая метод poesh(A) класса idi. Закомментил - лень писать
  };
   
  class B{...};       //Про класс ничего крутого не придумал, но у меня есть отмаза: Быков тоже.
  
  int get_a(A p){   //Посмотри в скобку для аргументов. Если туда не передать ничего, то зачем вообще эта дружба?
    return p.a;     //ДА, БЛ*ТЬ, Я СДЕЛАЛ ДРУГОМ ГЕТТЕР. И ЧТО ТЫ МНЕ СДЕЛАЕШЬ?
  }

  int main(){
    A ga;
    cout << get_a(ga);
  }

__________________________________________________________________

Билет 8. Шаблоны классов. Примеры использования

Шаблон класса - синтаксическая конструкция, позволяющая на этапе компиляции генерировать класс по указанным аргументам шаблона. Пример:

template <class T, size_t S>  // S можно передать только обычным числом, никаких переменных!
class SimpleArray             // Шаблонный класс. Аргументы шаблона: тип, количество
{
    T array[S];
public:
    ...     // что-нибудь с ним можно поделать
};
Cпасибо за этот билет Толяну, мировой мужик!

__________________________________________________________________

Билет 9-11. Перегрузка стандартных операций. пример.

Под операциями подразумеваются всякие штуки с участием операторов (+, -, =, *, /, <<...)

Перегрузка операций в языке Си++ это возможность распространения действия стандартных операций на операнды, для которых эти операции первоначально не предназначались. Это возможно, если хотя бы один из операндов является объектом класса, для этого создается специальная, так называемая, оператор-функция (Какая-то заумная хрень от Быкова. Если по-простому, то перегружать можно, если есть класс).

Список операторов, которые можно перегружать: (это не для вопроса, а для общего развития)

Унарные арифметические: + и - 
Бинарные арифметические: +, -, *, /, %
Сравнение: ==, !=, <, >, <=, >=
Логические унарные: !
Логические бинарные: &&, ||
Побитовые унарные: ~
Побитовые бинарные: &, |, ^, <<, >>
Инкремент и декремент: ++а, а++, --а, а--
Присваивание: = (не перегружайте, если не чувствуете себя большим специалистом)
Составное присваивание: х=, где х - любая бин.оп. кроме сравнения (можно перегружать свободно)

Любое приведение типа: (тип) a  (это как с++ за вас приводит int к double, если надо)
Квадратные скобки (бинарный): х[у]
Круглые скобки (n-арный): х(у) (скобки можно перегрузить на любое количество аргументов)
Переход по указателю (унарный): *x
Получение адреса (унарный): &x
"От указателя к полю": ->, ->*
Работа с памятью: new, new..[], delete, delete[]
Запятая: ,

Список операторов, которые нельзя перегружать:

Получение поля: . и .*
Тернарный оператор: ?:
Область видимости: :: (как в std::cout)
Оператор получения занятой памяти: sizeof
Оператор получения типа: typeid() (см.34 вопрос)
Оператор alignof
Указания препроцессору: # и ##

Способы перегрузки:

  1. Описать оператор в классе (Очевидно, статус доступа: public)
  2. Описать оператор вне класса (и, при желании, назначить его другом класса)

Различия перегрузки способами 0 и 1:

Если мы перегружаем операцию вне класса, то она принимает столько аргументов, сколько надо.
Если же перегружаем как член класса, то она принимает на один аргумент меньше, один всегда занят классом.
То есть унарные в классе не принимают аргументов, а вне класса - принимают один (и тот - сам класс)
Бинарные в классе принимают один аргумент, а вне класса - принимают два (среди которых один - это класс)

Если перегружаем оператор в классе, и он бинарный, то перегружаются обе постановки:
  Студент+число     //пример для класса студент, оператора + и второго операнда представленного числом
  Число+студент
Если же вне класса, то придётся перегружать каждую постановку вручную
  Студент+число - первая перегрузка
  Число+студент - вторая перергрузка

Правила перегрузки:

  1. Не придумывать новых операторов
  2. Бинарный оператор перегружаем как бинарный, унарный как унарный
  3. #include "Список операторов, которые нельзя перегружать"
  4. Операции "=", "[]", () и -> перегружаются обязательно как функции класса
  5. #include "Различия перегрузки способами 0 и 1"

Пример:

  class Vector2{
    int x;
    int y;
  public:
     Vector2 operator-();                    //В классе
     friend int operator*(Vector2, Vector2); //Вне класса
  };
  
  Vector2 Vector2::operator-(){
     Vector2 m;
     m.x = -x;
     m.y = -y;
     return m;
  }
  int operator*(Vector2 a, Vector2 b){
    return (a.x*b.x + a.y*b.y);
  }

Особенности перегрузки инкремента и декремента (БИЛЕТ 11)

//Префиксная - слева, постфиксная - справа
//Инкремент и декремент СЛЕВА перегружаются просто как унарные операторы
//Инкремент и декремент СПРАВА получают фиктивный второй параметр типа int (нигде не используется). 
//  Пример:
  Vector operator++(Vector a1, int) 
  {
    Vector a2=a1;
    a1.x++;
    a1.y++;
    return a2;
  }
Дополнительный материал (не к билету) - перегрузка преобразования типов
class Date{
    char days;
    char months;
    unsigned short years;
public:
    Date(char d=1, char m=1, unsigned short y=1970)
    { days=d;  months=m;  years=y; }
    operator string(){          //Оформление правильное - я компилировал, я проверял
        string p=" ";
        if(days%10==days) p+="0";
        p+=to_string(days);
        p+=".";
        if(months%10==months) p+="0";
        p+=to_string(months);
        p+=".";
        p+=to_string(years);
        return p.substr(1,10);
    }
    operator int(){  return years;  }
};


int main(){
    Date a;
    cout<<string(a)<<endl;
    //Без подсказки железка не понимает моего желания перевести в string, с cout.
    //Даже если других преобразований не прописано.
    //Зато в функцию с аргументом-string передавать можно без подсказки.
    cout<<a;   //В int умеет :)
}

__________________________________________________________________

Билет 12. Особенности перемещения объекта, продемонстрировать пример перемещения при перегрузке операции «=» (присваивание) на примере своего класса вектор, содержащем в качестве поля динамический массив элементов типа int.

-- Заимствование из 3 билета-- Конструктор перемещения - это конструктор вида (для случая внутри класса):

A(A&& a){               //A - имя класса
  Операторы_тела конструктора
}
Назначение конструктора перемещения: взять поля в исходном объекте и перекинуть в новый так, чтобы исходный можно было свободно уничтожить, не повредив новый. Пример реализации - скопировать поля в новый и занулить указатели в исходном, чтобы его деструктор не добрался до важных объектов.

Вызов конструктора перемещения в коде программы:

  A a1;
  A a2 = std::move(a1);

--Конец заимствования--

Но работать мы будем не с конструктором, а с присваиванием перемещением. Тут почти всё то же самое.

Давайте для начала введём наш класс:

  class MyVector{
  public:
    int *coords;
    int length;
    MyVector(){ length=0; coords=0;}
    void operator=(MyVector&&);     //Кстати здесь мы сломали присваивание. Без move() оно больше не работает
    void print();
  };


void MyVector::print(){
    if(length==0) cout<<"empty"<<endl;
    for(int i=0; i<length; i++)
        cout<<coords[i]<<endl;
}

void MyVector::operator=(MyVector&& a){
    if(coords!=0) delete[] coords;       //Совершенно необязательная чистка памяти
    coords = a.coords;
    length = a.length;
    a.coords = 0;
    a.length = 0;
}

А затем добавим капельку intmain-а: (можете сами что-нибудь поинтереснее придумать)

int main(){
    int len = 3;
    MyVector r,l;
    r.length = len;
    r.coords = new int[len];
    for(int i=0; i<r.length; i++)
        r.coords[i] = i;
    cout<<"r:";
    r.print();
    l=move(r);   //Быков как-то это без move делает, но это блин фокус какой-то
    cout<<"r:";
    r.print();
    cout<<"l:";
    l.print();
}

После этого примера (отклеятся обои) программа выведет r: 0 1 2, затем присвоит, выведет r: empty и l: 0 1 2

__________________________________________________________________

Билет 13. Наследование классов. Объявление производного класса, пример использования.

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

Для наследования после названия класса ставится двоеточие, и дальше имя класса, от которого наследуемся:

  class A{
    (...)
  };
  
  class B: A{
    (...)
  };

Перед именем базового класса может указываться статус доступа наследования или тип наследования доступа (public,protected, private). Это определяет статус доступа наследуемых полей и методов из родителя внутри производного. Производный класс может определяться с ключевым словом struct или class (с union нельзя). Если производный класс определен с ключевым словом class, то по умолчанию статус доступа наследования private. Если struct, то public.

Пример:

  class Square{
    int a;
    short sides;
  public:
    Square(int arg){ a=arg; sides=4;}
    int getP() {return a*sides;}
    int getS() {return a*a;}
    short getSides(){return sides}
  };
  
  class Triangle: public Square{  //ДА, БЛ*ТЬ, Я НАСЛЕДУЮ ТРЕУГОЛЬНИК ОТ КВАДРАТА! И ЧТО ТЫ МНЕ СДЕЛАЕШЬ?
    int b;
    int c;
  public:
    Trinagle(int pa, int pb, int c){
      a=pa;
      b=pb;
      c=pc;
      sides=3;
    }
    int getP(){ return a+b+c;}
    int getS(){ 
      double p=1.0*getP()/2;
      return sqrt(p*(p-a)*(p-b)*(p-c));
    }
  };

__________________________________________________________________

Вопрос 14. Статусы доступа при наследовании классов.

Перед именем базового класса может указываться статус доступа наследования или тип наследования доступа (public,protected,private). Это определяет статус доступа наследуемых полей и методов из родителя внутри производного. Производный класс может определяться с ключевым словом struct или class (с union нельзя). Если производный класс определен с ключевым словом class, то по умолчанию статус доступа наследования private. Если struct, то public.

исходный -> public protected private
public-наследование public protected private
protected-наследование protected protected private
private-наследование private private private

__________________________________________________________________

Вопрос 15. Конструкторы и деструктор в производных классах, примеры.

Конструктор производного класса всегда должен вызывать конструктор базового класса. Если это действие не написано программистом, то вызывается конструктор без параметров (если его нет, вылезает ошибка). Если класс имеет несколько базовых, то конструкторы базовых классов должны вызываться в порядке перечисления этих классов в списке базовых.

Деструктор производного класса всегда неявно по умолчанию после выполнения своего тела вызывает деструкторы базовых классов. Причем порядок вызова деструкторов обратен порядку вызова конструкторов.

class Base
{
    int *first;

public:
    Base(int size)
        : first(new int[size])
    {}

    virtual ~Base()
    {
        delete[] first;
    }
}

class Derived
{
    int *second;

public:
    // Сначала вызовется конструктор базового класса
    Derived(int size)
        : Base(size), second(new int[size])
    {}

    // Сначала будет вызван деструктор производного класса
    virtual ~Derived()
    {
        delete[] second;
    }
}

__________________________________________________________________

Вопрос 16. Множественное наследование и виртуальные базовые классы, примеры.

Множественное наследование - наследование более чем от одного класса

struct Base
{
    int a = 5;
};

// Применим виртуальное наследование
struct A: virtual public Base
{
    int b = 6;
};

// Применим виртуальное наследование
struct B: virtual public Base
{
    int c = 8;
};

struct Z: public A, public B
{
    int getSum() const
    {
        return a + b + c;
    }
};

Виртуальное наследование утяжеляет класс на один указатель (таблица виртуальных классов), т.е. на sizeof(int).

Однако, если не наследоваться виртуально, то класс Z будет содержать два экземпляра Base, и обращение z->a будет кидать ошибку, потому что клас Z получил a от Base и через A, и через B, и ему непонятно, про какую из двух a вы говорите.

__________________________________________________________________

Вопрос 17. Переопределение функций при наследовании классов, виртуальные функции в Си++ (таблица виртуальных функций). Статическое и динамическое связывание. Примеры.

class Base
{
public:
    virtual int dynamicLink() const
    {
        return 5;
    }

    int staticLink() const
    {
        return -5;
    }
};

class Derived
{
public:
    int dynamicLink() const override
    {
        return Base::dynamicLink() + 4;
    }

    int staticLink() const
    {
        return Base::staticLink() - 4;
    }
}

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

// Где-то в коде
Derived d;

// Напечатает 9, так как произошло динамическое связывание
std::cout << ((Base *)(&d))->dynamicLink() << std::endl;

// Напечатает -5, так как произошло статическое связывание
// Т.к. метод staticLink не виртуальный
std::cout << ((Base *)(&d))->staticLink() << std::endl;

__________________________________________________________________

Вопрос 18. Абстрактные и локальные классы в Си++, примеры.

Абстрактный класс - класс, содержащий хотя бы одну чистую виртуальную функцию. Объект такого класса нельзя создать, но можно создать указатель на него.

class AbstractItem
{
    int weigth;
    int amount;

public:
    virtual std::string getItemName() const = 0;
}

Локальный класс - класс, объявленный не в глобальной области (в блоке или в теле другого класса)

int main()
{
    struct A
    {
        int a = 0;
        int b = 0;
    };
}

__________________________________________________________________

Вопрос 19. Понятие особой (исключительной) ситуации в Си++. Общий формат обработки исключительных ситуаций (ключевые слова: try, catch, throw).

Исключительная ситуация - тот момент, когда дальнейшее выполнение программы невозможно.

Обработка исключений, возбужденных оператором throw, идет по следующей схеме:

  1. Создается статическая переменная со значением, заданным в операторе throw. Она будет существовать до тех пор, пока исключение не будет обработано. Если переменная-исключение является объектом класса, при ее создании работает конструктор копирования.
  2. Завершается выполнение защищенного try-блока: раскручивается стек подпрограмм, вызываются деструкторы для тех объектов, время жизни которых истекает и т.д.
  3. Выполняется поиск первого из catch-блоков, который пригоден для обработки созданного исключения.

Общий формат описан ниже

__________________________________________________________________

Вопрос 20. Обработка исключения внутри функции, в которой сгенерировано исключение (ключевые слова: try, catch, throw), пример.

void exceptionFunction()
{
    try {
        // Что-то делаем и вдруг
        throw std::length_error("Text");
    } catch (std::length_error &e) {
        std::cout << "Captured" << std::endl;
    }
}

__________________________________________________________________

Вопрос 21. Обработка исключения вне функции, в которой сгенерировано исключение, объявление функции, в которой генерируются исключения (ключевые слова: try, catch, throw), пример.

void exceptionFunction()
{
    // Что-то делаем
    // Возможна генерация std::length_error, std::out_of_range и каких-либо других исключений, надклассом которых является std::exception
}

int main()
{
    try {
        exceptionFunction();
    } catch (std::length_error &e) {
        std::cout << "Length error: " << e.what() << std::endl;
    } catch (std::out_of_range &e) {
        std::cout << "Out of range: " << e.what() << std::endl;
    } catch (std::exception &e) {
        std::cout << "Other exception: " << e.what() << std::endl;
    }
}

__________________________________________________________________

Вопрос 22. Создание исключения как объекта класса в Си++ (ключевые слова: try, catch, throw), пример.

std::length_error("Text");       // Создание исключения как объекта класса

try обозначает начало блока с возможным выборосом исключения

catch( ) обозначает начало блока с обработкой исключений

throw выкидывает исключение

__________________________________________________________________

Вопрос 23. Формы обработчиков исключений (catch), примеры.

catch (std::exception &e)       // Ловим определенное исключение по его классу или надклассу
catch (...)         // Ловим любое исключение

__________________________________________________________________

Вопрос 24. Формы выражений генерации исключений в Си++ (throw), примеры.

throw std::out_of_range("Информативная информация");        // Выкидывание
// Перешлет существующее исключение, либо
// SIGABRT приведет к аварийному закрытию программы
throw;

__________________________________________________________________

Вопрос 25. Обработка исключений в стандартной библиотеке Си++: обзор основных классов

Надклассы

Класс Описание
std::exception Базовый класс
std::runtime_error Ошибка времени выполнения
std::logic_error Логическая ошибка

Некоторые из исключений

Класс Описание
std::out_of_range Выход за границу
std::length_error Нарушен предел длины
std::bad_alloc Не смогли аллоцировать память
std::bad_cast Ошибка преобразования ссылок в dynamic_cast

Дерево

__________________________________________________________________

Вопрос 26. Обработка исключений в стандартной библиотеке Си++: создание своего класса исключения на основе класса std::exception, пример.

Обработка исключений производится с помощью конструкции try { ... } catch ( ... ) { ... }

// Свое исключение
class MyException: public std::exception
{
public:
    const char *what() const noexcept override
    {
        return "My exception raised!";
    }
};

// Где-то в коде
try {
    myFunction();   // Пусть эта функция может выбросить MyException
} catch (const MyException &e) {
    // Либо можно ловить ``const std::exception &e`` так как это надкласс
    std::cout << e.what() << std::endl;
}

__________________________________________________________________

Вопрос 27. Многозадачность в стандартной библиотеке C++: высокоуровневый интерфейс (функция std::async() и шаблон класса std::future< >), пример создания потока и получение результата потоковой функции.

std::async позволяет выполнять функцию реально или мнимо параллельно.

std::future предоставляет механизм взаимодействия с результатом асинхронной функции.

#include <future>

int counter()
{
    int result = 0;
    for (int i = 0; i < 10000; i++) {
        result++;       // Что-то делаем
    }
    return result;
}

int main()
{
    std::future<int> handle = std::async(counter);
    std::cout << handle.get() << std::endl;
}

__________________________________________________________________

Вопрос 28. Многозадачность в стандартной библиотеке C++: низкоуровневый интерфейс (использование класса std::thread), пример.

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

#include <thread>

void counter(int *num)
{
    // Что-то делаем
    for (int i = 0; i < 10000; i++) {
        num++;
    }
}

int main()
{
    int number = 0;
    std::thread thread(counter, &number);

    // Что-то делаем
    
    thread.join();  // Фиксируем прибыль (ждем, пока поток закончит)
    std::cout << number << std::endl;
}

__________________________________________________________________

Вопрос 29. Синхронизация в стандартной библиотеке C++. Использование класса std::mutex (взаимное исключение), пример.

Основной способ синхронизации потоков - std::mutex

#include <mutex>

// Общие данные
std::mutex mutex;

// Thread 1
for (int i = 0; i < 100; i++) {
    mutex.lock();           // Блокирует. Если уже заблокировано, то поток стоит и ждем
    // Гарантируем, что вывод не "перемешается"
    std::cout << "Part" << " " << "of" << " " << "text" << "." << "." << std::endl;
    mutex.unlock();
}

// Thread 2
for (int i = 0; i < 100; i++) {
    mutex.lock();           // Блокирует. Если уже заблокировано, то поток стоит и ждем
    // Гарантируем, что вывод не "перемешается"
    std::cout << "Part" << " " << "of" << " "
              << "the" << " "<< "second" << "." << "." << std::endl;
    mutex.unlock();
}

__________________________________________________________________

Вопрос 30. Использование для синхронизации потоков блокировок – шаблона std::lock_guard, пример.

Использование std::lock_guard позволяет заблокировать std::mutex в стиле RAII

#include <mutex>

// Где-то выше
std::mutex mutex;

// Где-то в коде
{
    std::lock_guard<std::mutex> lock(mutex)
    // Делаем что-то небезопасное 
}        // При попадении в исключение или при выходе из блока само unlock'нется

__________________________________________________________________

Вопрос 31.Использование условных переменных (объекты класса std::condition_variable) для синхронизации потоков в стандартной библиотеке C++: посылка и принятие оповещений потоками, примеры.

Назначение std::condition_variable списываем из вопроса :)

#include <condition_variable>
// ...

// Общие переменные
std::mutex mutex;
std::condition_variable cv;
bool isDone = false;

// Thread 1
std::unique_lock<std::mutex> lock(mutex);
while (!isDone) {       // Исключаем ложные срабатывания
    cv.wait(lock);      // Сработает, когда thread 2 пошлет
}
// Done!

// Thread 2
std::unique_lock<std::mutex> lock(mutex);
sleep(5);           // Что-то делаем
isDone = true;
cv.notify_one();

__________________________________________________________________

Вопрос 32. Использование атомарных операций (шаблон std::atomic) для синхронизации потоков в стандартной библиотеке C++

Поведение объектов шаблонов std::atomic, при использовании в нескольких потоках, определено стандартом. То есть, рекомендуется их использовать при взаимодействии с несколькими потоками

#include <iostream>
#include <thread>
#include <atomic>
 
std::atomic<long long> data;

void do_work()
{
    data += 1;
}
 
int main()
{
    std::thread th1(do_work);
    std::thread th2(do_work);
    std::thread th3(do_work);
    std::thread th4(do_work);
    std::thread th5(do_work);
 
    th1.join();
    th2.join();
    th3.join();
    th4.join();
    th5.join();
 
    std::cout << "Result:" << data << '\n';
}

__________________________________________________________________

Вопрос 33. Дополнительные операции преобразования типов: const_cast, dynamic_cast, static_cast, reinterpret_cast.

const_cast - снимает константность

const char *str = "hello";
char *str1 = const_cast<char*>(str);

dynamic_cast - преобразовывает типы в рантайме (полезно для преобразования указателей/ссылок базовых классов к производным). Ошибкит: При преобразовании указателя базового к производному - nullptr, ссылка - std::bad_cast

class Base
{
public:
    virtual ~Base() = default;
};

class Derived: public Base {};

int main()
{
    Base *ptr;
    // ... что-то кладем в ptr
    auto derived = dynamic_cast<Derived *>(ptr);
}

static_cast - преобразование типов, проверяемое на этапе компиляции

double b = static_cast<double>(777);

reinterpret_cast - самое небезопасное преобразование. Берет участок памяти и бахает под другой тип. Ничего не проверяет

auto waitWhat = *reinterpret_cast<void**>(777);

__________________________________________________________________

Вопрос 34. Понятие о динамической идентификации типов в Си++. Операция typeid. Основные функции класса typeinfo. Примеры

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

typeid - идентификатор типа

typeinfo - позволяет получить объект типа std::type_info, который содержит хэш типа, название типа

В обе функции передается любой объект

AbstractClass *ptr;
std::cout << typeid(*ptr).name() << std::endl;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment