UPD: РЕЖИМ БЕЗУДЕРЖНОГО СПИСЫВАНИЯ (удобного поиска). Теперь вопросы тематически раскиданы по разным гистам.
Основы (1-2 и 4-5)
Перегрузки (9-12)
Наследование (13-18)
Исключения (19-26)
Потоки (27-31)
Остальное (6-8 и 33-34)
Задачи (От Никиты Першаева и Жени)
Вопросы (Сайт Быкова)
Внимание! Этот гист больше не будет обновляться! Все правки будут вноситься в тематические гисты.
Автор билетов 14-34: Толик
Билет 1. Классы в языке С++: объявление, поля и методы классов. Определение функций внутри класса и за пределами класса, примеры.
Класс - это структура, в которую в языке С++ введены методы для обработки полей.
- struct
- class
- union //Не самое полезное слово, но в лекциях у Быкова было
Add: аффтар только что сползал создал union, и оно приняло метод. Всё работает, union тоже умеет в класс.
<ключевое слово> <имя класа>{
<компоненты>
};
В качестве ключевого слова может использоваться как 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;
}
Свойство доступности определяет возможность доступа к полям и методам за пределами класса (через имя объекта или через указатель на объект или в производном классе).
- 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);
Перед полем или методом можно дописать ключевое слово static - тогда он(о) станет станет статическим. На статические поля и методы распространяются модификаторы доступа.
Статическое поле существует в единственном числе вне зависимости от количества объектов класса. Для всех них оно одинаково. Получить к нему доступ можно либо через имя класса (A::a
), либо через имя какого-нибудь объекта этого класса. Инициализировать статическое поле надо вне класса, потому что иначе будут происходить всякие странности вроде переинициализации поля с каждым созданным объектом.
Статический метод - такой метод, который может быть вызван по имени класса, даже если не создано ни одного объекта этого класса. Он не взаимодействует ни с какими полями и методами класса кроме статических. К нем можно обращаться точно так же, как к ст.полю.
Из курса алгоритмических языков мы знаем, что функции хранятся в стеке, а ещё при вызове не-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 БЫКОВУ ЭТО НЕ НАЗЫВАЙТЕ.
Чтобы обратиться не к компоненту класса, а сразу к указателю на него используются операторы ".*" и "->*". Перед использованием этих операторов сами указатели надо ещё настроить.
Шаг 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 (сработает для другого метода)
}
"Другом" класса называется функция или другой класс, НЕ ЯВЛЯЮЩИЕСЯ его методами(полями), но имеющие доступ к его приватным полям. Описать друга в классе легко - напишите его заголовок в классе с модификатором 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);
}
Шаблон класса - синтаксическая конструкция, позволяющая на этапе компиляции генерировать класс по указанным аргументам шаблона. Пример:
template <class T, size_t S> // S можно передать только обычным числом, никаких переменных!
class SimpleArray // Шаблонный класс. Аргументы шаблона: тип, количество
{
T array[S];
public:
... // что-нибудь с ним можно поделать
};
Под операциями подразумеваются всякие штуки с участием операторов (+, -, =, *, /, <<...)
Перегрузка операций в языке Си++ это возможность распространения действия стандартных операций на операнды, для которых эти операции первоначально не предназначались. Это возможно, если хотя бы один из операндов является объектом класса, для этого создается специальная, так называемая, оператор-функция (Какая-то заумная хрень от Быкова. Если по-простому, то перегружать можно, если есть класс).
Унарные арифметические: + и -
Бинарные арифметические: +, -, *, /, %
Сравнение: ==, !=, <, >, <=, >=
Логические унарные: !
Логические бинарные: &&, ||
Побитовые унарные: ~
Побитовые бинарные: &, |, ^, <<, >>
Инкремент и декремент: ++а, а++, --а, а--
Присваивание: = (не перегружайте, если не чувствуете себя большим специалистом)
Составное присваивание: х=, где х - любая бин.оп. кроме сравнения (можно перегружать свободно)
Любое приведение типа: (тип) a (это как с++ за вас приводит int к double, если надо)
Квадратные скобки (бинарный): х[у]
Круглые скобки (n-арный): х(у) (скобки можно перегрузить на любое количество аргументов)
Переход по указателю (унарный): *x
Получение адреса (унарный): &x
"От указателя к полю": ->, ->*
Работа с памятью: new, new..[], delete, delete[]
Запятая: ,
Получение поля: . и .*
Тернарный оператор: ?:
Область видимости: :: (как в std::cout)
Оператор получения занятой памяти: sizeof
Оператор получения типа: typeid() (см.34 вопрос)
Оператор alignof
Указания препроцессору: # и ##
- Описать оператор в классе (Очевидно, статус доступа: public)
- Описать оператор вне класса (и, при желании, назначить его другом класса)
Если мы перегружаем операцию вне класса, то она принимает столько аргументов, сколько надо.
Если же перегружаем как член класса, то она принимает на один аргумент меньше, один всегда занят классом.
То есть унарные в классе не принимают аргументов, а вне класса - принимают один (и тот - сам класс)
Бинарные в классе принимают один аргумент, а вне класса - принимают два (среди которых один - это класс)
Если перегружаем оператор в классе, и он бинарный, то перегружаются обе постановки:
Студент+число //пример для класса студент, оператора + и второго операнда представленного числом
Число+студент
Если же вне класса, то придётся перегружать каждую постановку вручную
Студент+число - первая перегрузка
Число+студент - вторая перергрузка
- Не придумывать новых операторов
- Бинарный оператор перегружаем как бинарный, унарный как унарный
- #include "Список операторов, которые нельзя перегружать"
- Операции "=", "[]", () и -> перегружаются обязательно как функции класса
- #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);
}
//Префиксная - слева, постфиксная - справа
//Инкремент и декремент СЛЕВА перегружаются просто как унарные операторы
//Инкремент и декремент СПРАВА получают фиктивный второй параметр типа 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
Основная идея: на базе существующего класса (называется родитель или базовый), создается производный класс (наследник или дочерний), который включает в себя поля и функции базового класса (наследует их), и содержит дополнительные поля (обладает новыми свойствами) и функции.
Для наследования после названия класса ставится двоеточие, и дальше имя класса, от которого наследуемся:
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));
}
};
Перед именем базового класса может указываться статус доступа наследования или тип наследования доступа (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 |
Конструктор производного класса всегда должен вызывать конструктор базового класса. Если это действие не написано программистом, то вызывается конструктор без параметров (если его нет, вылезает ошибка). Если класс имеет несколько базовых, то конструкторы базовых классов должны вызываться в порядке перечисления этих классов в списке базовых.
Деструктор производного класса всегда неявно по умолчанию после выполнения своего тела вызывает деструкторы базовых классов. Причем порядок вызова деструкторов обратен порядку вызова конструкторов.
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;
}
}
Множественное наследование - наследование более чем от одного класса
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;
Абстрактный класс - класс, содержащий хотя бы одну чистую виртуальную функцию. Объект такого класса нельзя создать, но можно создать указатель на него.
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, идет по следующей схеме:
- Создается статическая переменная со значением, заданным в операторе throw. Она будет существовать до тех пор, пока исключение не будет обработано. Если переменная-исключение является объектом класса, при ее создании работает конструктор копирования.
- Завершается выполнение защищенного try-блока: раскручивается стек подпрограмм, вызываются деструкторы для тех объектов, время жизни которых истекает и т.д.
- Выполняется поиск первого из 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
выкидывает исключение
catch (std::exception &e) // Ловим определенное исключение по его классу или надклассу
catch (...) // Ловим любое исключение
throw std::out_of_range("Информативная информация"); // Выкидывание
// Перешлет существующее исключение, либо
// SIGABRT приведет к аварийному закрытию программы
throw;
Класс | Описание |
---|---|
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();
}
Использование 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;