Д.з. 1
Задача 3: список struct list { int val; list* next; list(int val_, list* next_) : val(val_), next(next_) {} }; void print(const list* p) { while (p != nullptr) { cout val next; } } // Добавление list* newel = new list; newel->val = v; newel->next = first; first = newel; Или можно короче first = new list(v, first);
Задача 3: список (продолжение) void add(list* & p, int v) { p = new list(p, v); } // Пример вызова list* first = nullptr; for (int i = 0; i
Задача 1: равнобедренный треугольник class triang { protected: int x, y; // Координаты // середины основания int h, w; // Длина основания public: // и высота triang(int x_, int y_, int h_, int w_); virtual void draw() const; virtual ~rhomb() {} // Лучше так! }; // Вспомогательная функция, // рисует отрезок void draw_line(int x1, int y1, int x2, int y2) { cout
Задача 1: равнобедренный тругольник - продолжение class crossed_triang : public triang { public: crossed_ triang(int x_, int y_, int h_, int w_); void draw() const; }; crossed_ triang ::crossed_ triang( int x_, int y_, int h_, int w_) : triang(x_, y_, h_, w_) {} void crossed_ triang::draw() const { // "Унаследованное" изображение triang::draw(); // Еще линии draw_line(x + w/4, y, x + w/4, y + h/2); draw_line(x - w/4, y, x - w/4, y + h/2); } // Пример вызова: // Так мы проверим, что виртуальная // функция вызывается правильно rhomb* p = new crossed_ triang(30, 40, 10, 5); p->draw(); Типичные ошибки: Нет virtual
Еще про наследование 6
Когда используется то, что функция виртуальная? 1. Доступ через указатель time* p; if (…) p = new time(8, 30); else p = new time_with_sec(8, 30, 15); p->print(); // М.б. разные типы 2. Доступ через параметр-ссылку void my_print(time& t) { cout
Когда используется то, что функция виртуальная? (Продолжение) 4. Метод в базовом классе class time { … void my_print() { cout
Еще другими словами: как понимать слово virtual... Если нам дали класс, в котором есть виртуальная функция: class abc { virtual void f(); Это надо читать так: «… Функцию f вы можете переопределить и задать свой вариант…» 9
Если в документации написано virtual... = 0; class abc { virtual void f() = 0; Это надо читать так: «… Функцию f вы должны переопределить и задать свой вариант. …» 10
Можно ли конструктор обьявлять virtual? Нет! Когда нужна виртуальная функция? Код должен работать с разными обьектами … p->print(); // И для time // и для time_with_sec Для конструктора так не бывает: time t; p = new time(); Тип всегда известен (Есть понятие виртуальный конструктор – но это просто такой прием программирования) Техническая проблема – пока работает конструктор, ссылка на vtable еще не установлена Следствие: в конструкторе лучше не вызвать виртуальные функции 11
Можно ли деструктор обьявлять virtual? Да, и почти всегда надо! class shape {... }; class huge_shape : public shape { int* p; huge_shape() { p = new int [100000];... } ~huge_shape() { delete [] p; } }; // Если деструктор // не виртуальный: shape* p;... p = new huge_shape();... delete p; // Вызывается дестр-р // shape, а не huge_shape! // Утечка памяти! 12
Виртуальные деструкторы - продолжение А как правильно? В определении класса shape:... virtual ~shape() {} … Правило: Есть производные классы лучше определить в базовом классе деструктор (даже пустой), и объявить его виртуальным. или, другими, словами, Не имеет смысл определять виртуальный деструктор, только если для класса вообще не предполагается определять производные классы. 13
Д.з. про исключения 14
Задача 4: ошибка в конструкторе time time::time(int h, int m): hour(h), min(m) { if (h =24 || m =60) throw "Ошибка в конструкторе time"; } // Пример вызова конструктора int i, j; cin >> i >> j; try { time t(i, j); t.print(); } catch (const char* s) { cout
Задача 4: еще вариант #include // Стандартные исключения time::time(int h, int m): hour(h), min(m) { if (h =24 || m =60) throw out_of_range("Oшибка в конструкторе time"); } // Пример вызова конструктора try { time t(i, j); t.print(); } catch (const out_of_range& ex) { cout
Задача 5: исключения и утечка памяти // Исходная функция // … что-то делает со стеком … // Если стек пуст – исключение void f(stack& s) { int* p = new int[1000]; … что-то делаем с p … cout
Задача 5: еще вариант // Вариант 2: void f(stack& s) { int* p = new int[1000]; … что-то делаем с p … try { cout
Задача 5: еще вариант – "обернуть" p в класс! // Вариант 3: class wrapper { int* p; public: wrapper() { p = new int[1000]; } ~wrapper() { delete[] p; // Теперь delete точно выполниться } int* get() { return p; } }; void f(stack& s) { wrapper p; … что-то делаем с p.get() … cout
RAII Resource Acquisition Is Initialization Получение ресурса есть инициализация Пусть обязательно надо выполнить какое-то действие в конце функции или при выходе из блока (удалить память, закрыть файл, восстановить форму курсора и т.д.) Совет: завести вспомогательный класс выполнять это действие в деструкторе Преимущества: Получается короче Exception safe – действие будет выполнено даже при наличии исключений Замечание: Если действие – это освободить память то можно использовать стандартные классы (vector) или стандартныеумные указатели (scoped_ptr, shared_ptr)
Еще про исключения 21
Какие вообще бывают ошибки и как их обрабатывать? 1. Ошибка, которой в отлаженной программе быть не должно i = *p; // А если p == 0? assert(условие); #include … assert(p != 0); Если условие не выполнится –> сообщение Только в Debug версии 2. Ошибка, которую можно разумно обработать / исправить: Код завершения bool ok = f(); if (ok) 3. Ошибка, для которой разумная обработка – это завершение работы (в каком-то смысле): throw
Константы в классах 23
Пример, когда с const все не так просто class strange_shape { … }; double strange_shape::area() { … полчаса вычислений… } Чтобы не считать каждый раз: strange_shape() : saved_area(0) {…{… double strange_shape::area() { if (saved area == 0) { saved_area = … полчаса вычислений… } return saved_area; } Прием: кеширование (cache) 24
mutable поля class strange_shape { … double strange_shape::area() const { … saved_area = …; // Изменение поля в константном методе??!! } … mutable int saved_area; }; mutable поле – можно менять в константных методах 25
Как сделать чтобы классом было удобнее пользоваться? 26
Будем определять класс complex class complex { double re, im; public: complex(double re_ = 0, double im_ = 0) : re(re_), im(im_) {} … Хотелось бы: - определения +, +=, *, *= - преобразования double -> complex и т.д. 27
friend Пример: синус complex c1, c2; c1 = sin(c2); complex sin(complex c) { … c.re … c.im … // ??? Другой вариант: sin, как метод c.sin(); // Неудобно.. friend class complex { double re, im; public: friend complex sin(complex c);... }; complex sin(complex c) { … c.re … c.im … // ОК 28
friend - продолжение friend объявление функции; Пишется внутри class Означает: Функция имеет доступ к private и protected полям и методам friend класс; Все методы класса – друзья class abc { … friend klm; }; // Все методы класса klm – // друзья класса abc. Не наследуется Не транзитивно. 29
Как задать свой оператор complex c1, c2, c3; c1 = c2 + c3; Можно определять функции и методы с именем: operator Например, operator+, operator
Ограничения при перегрузке операторов Только существующие операторы operator** - ошибка Стандартные приоритеты Один из операндов – class или struct (или enum или union) или ссылка на них Нет связи + и += и ++, или и т.д. Но см. boost::operators 31
Замечания Свои операторы – мне кажется, только если очевидно станет удобнее Бывают особые случаи, когда надо определить (следующие темы) 32
Как лучше задать operator+ ? 2 способа: Метод: class complex { complex operator+(complex c) { … } } c1 + c2 c1.operator+(c2); Обычная функция complex operator+(complex c1, complex c2) {… } c1 + c2 operator+(c1, c2); Правило: такие операторы, как правило, лучше определять, как обычную функцию. 33
Оператор + для complex // Вариант 1 complex operator +(complex c1, complex c2) { complex res; res.re = c1.re + c2.re; res.im= c1.im + c2.im; return res; } В классе complex: friend complex operator+(complex c1, complex c2); // Вариант 2 complex operator+(complex c1, complex c2) { complex res(c1.re + c2.re, c1.im + c2.im); return res; } // Вариант 3 complex operator+(complex c1, complex c2) { return complex(c1.re + c2.re, c1.im + c2.im); } 34
Перегрузка операторов типа = c1 += c2; Лучше определять, как метод Напоминание: Все операции типа присваивания (=, +=, *=) возвращают ссылку на первый аргумент. n *= 10 // значение равно n, причем // это ссылка (n *= 10) + = 5 // тоже, что n = n*10 + 5; class complex { … complex& operator+= (complex c) { re += c.re; im += c.im; return *this; } В конце операторов типа = всегда пишут: return *this; 35
Как задать свои правила преобразования типов? 36
Преобразования типов Цель – писать: complex c = 3.5; c = 2.2; c += 3.1; Конструктор с 1 параметром задает преобразование типа complex(double re_ = 0, double im_ = 0) : re(re_), im(im_) {} // Примеры вызова c = complex(1.5); c = (complex)1.5; c = complex(1.5) 37
Неявные преобразования Преобразование типов иногда вызывается неявно: complex c = 1.5; complex c(1.5); c = 1.5; c = complex(1.5); Вызов функции: void f(complex c) { … } f(1.5); f(complex(1.5)) 38
Почему operator+ - не метод c1 = 1 + c;// Хотелось бы, чтобы так работало Если определим +, как обычную функцию c1 = operator+(1, c); // OK. Неявное преобразование, автоматически // преобразуется в // c1 = operator+(complex(1), c) Если определим +, как метод c1 = 1.operator+(c); // Ошибка. Не преобразуется в // c1 = complex(1).operator+(c); 39
Оператoр преобразования типа double x = (double)c; // Допустим, мы и так хотим // писать class complex { … operator double() const operator double() { return re;// Просто для }// примера.. }; operator имя-типа тип результата указывать не надо 40
Замечания про неявные прeобразования Замечания: д.б. однозначно (иначе будет ошибка компилятора) только одно (не подбираются цепочки преобразований) Точнее, одно определяемое + одно числовое complex c = 22; Когда применять? Мне кажется, только если очевидно, чтобы станет заметно удобнее. Компилятор не запутается, а вот вы можете 41
Д.з. 42
Д.з.-1 1.Для complex определить * и *= (Видимо, имеет смысл один из операторов определить через другой) 2. Определить структуру (или класс) «двоичное дерево». а. Ввести последовательность чисел, пока не будет введен 0, и создать из них упорядоченное двоичное дерево (дерево поиска). б. Напечатать все числа в вершинах дерева. 3. Определить класс rational (рациональное число) с полями числитель и знаменатель, и для него задать конструктор и преобразование в double: rational r(1,3); double x=r; // x = Задача 4 на следующем слайде 43
Д.з В реализацию стека добавить метод maxsin(), который считает максимум из синусов попарных произведений всех чисел на стеке. При этом давайте условно считать, что синус – это очень, очень долго работающая функция и заказчик просил вызывать ее как можно меньше. Поэтому заказчик просил: Производить вычисления только если вызывается max sin(), ничего не вычислять заранее. Запоминать результат и, если maxsin вызывается несколько раз подряд – просто возвращать запомненное значение. Еще начальник просил правильно расставить слова const и mutable Еще 0.5 балла, если при вычислении нового значения вы будете использовать последнее вычисленное ранее значение – хотя бы в некоторых случаях Замечания : Чтобы использовать sin надо в начале написать: #include Для простоты давайте считать, что функции top() у стека нет Если условие непонятно – пишите.. 44