1. Яким є недолік рекурсії у порівнянні з циклом?
2. Для чого використовують static змінні в функціях? Чи можна оголошувати static змінні в extern функціях?
3. Для чого всередині функцій використовують приведення до типу void?
int sum(int a, int b, int c) {
(void)c;
return a + b;
}
4. Наступний фрагмент коду виділяє пам’ять по вказівнику, але в ньому є помилка, яка призводить до витоку пам’яті. Знайдіть і виправте її.
void allocate(int* ptr, int size) {
ptr = new int[size];
}
int main() {
int* arr = nullptr;
int size = 10;
allocate(arr, size);
delete[] arr;
return 0;
}
Відповідь:
1. Яким є недолік рекурсії у порівнянні з циклом?
Кожен виклик функції вимагає алокації певної кількості пам’яті на стеку і ініціалізації змінних. Велика кількість рекурсивних викликів може призвести до помилки переповнення стеку — stack overflow. Якщо об’єктів багато, то і їх ініціалізація може вплинути на швидкодію. Цикл не має цих недоліків, бо працює з одними і тими ж змінними кожну ітерацію, пам’ять на стеку виділяється лише раз.
2. Для чого використовують static змінні в функціях? Чи можна оголошувати static змінні в extern функціях?
static змінні можуть використовуватись в різних контекстах, але суть та ж - збереження значення між викликами функцій. Окремі випадки застосування:
- Підрахунок кількості викликів функції.
- Просте кешування. Наприклад, обчислені факторіали чи числа Фібоначчі можна зберігати в масиві який оголошений як static всередині функції. Тоді можна повертати закешоване значення замість постійного переобчислення.
- Реалізація синглтону Мейерса - один з патернів проєктування.
- extern функції можуть мати static змінні, механізм залишиться тим же.
3. Для чого всередині функцій використовують приведення до типу void?
int sum(int a, int b, int c) {
(void)c;
return a + b;
}
Щоб уникнути помилок, на реальних проєктах зазвичай використовують досить жорсткі обмеження при компіляції. Зокрема вмикають опцію, щоб компілятор попереджав про невикористані параметри функції, і навіть переривав компіляцію в цьому випадку.
Таке ігнорування параметра необхідне здебільшого в процесі написання коду. Приведення до типу void "використовує" змінну з точки зору компілятора і цим самим обходить попередження. Таке приведення не генерує ніяких інструкцій, тобто не навантажує процесор без потреби.
4. Наступний фрагмент коду виділяє пам’ять по вказівнику, але в ньому є помилка, яка призводить до витоку пам’яті. Знайдіть і виправте її.
void allocate(int* ptr, int size) {
ptr = new int[size];
}
int main() {
int* arr = nullptr;
int size = 10;
allocate(arr, size);
delete[] arr;
return 0;
}
Функція allocate() приймає вказівник за значенням, він просто копіюється. Пам'ять буде виділена і значення ptr буде змінено коректно, але оскільки це лише копія, то присвоєння адреси не матиме впливу на вказівник arr - він так і залишиться nullptr. Як результат, delete[] буде викликано для nullptr, а до справді виділеної пам'яті вже не можна ніяк доступитись.
Виправити це просто - allocate() має приймати вказівник ptr за посиланням, тоді всі присвоєння до вказівника ptr будуть застосовані безпосердньо до arr:
void allocate(int*& ptr, int size)
Добавить комментарий