diff options
1 files changed, 120 insertions, 120 deletions
| -rw-r--r-- | src/teaching/upp-2025/cpp-pitfalls/index.du | 240 |
diff --git a/src/teaching/upp-2025/cpp-pitfalls/index.du b/src/teaching/upp-2025/cpp-pitfalls/index.du index 8a6936b..09b9339 100644 --- a/src/teaching/upp-2025/cpp-pitfalls/index.du +++ b/src/teaching/upp-2025/cpp-pitfalls/index.du @@ -1,37 +1,37 @@ -# Капани в C++ - УПП, КН, 2025-2026 +'def title Капани в C++ - УПП, КН, 2025-2026 +'def desc Помощна страница с кажи-речи всички странности и неприятности, които трябва да се знаят за УП курса. -'define expected-reading 15 min -'define created 22 November 2025 -'define edited 23 November 2025 -[$pagenav] +# {title} + +{pagenav 15 min;22 November 2025;23 November 2025} .warn Този документ ще бъде обновяван до края на курса. Моля, следете за промени! -[$br2] +{br2} :contents :numbered -1. [url #section-1 Лоши практики] +1. {uhref Лоши практики} .numbered - 1. [url #section-2 Записване/връщане на булеви стойности] + 1. {uhref Записване/връщане на булеви стойности} -2. [url #section-3 Оператори] +2. {uhref Оператори} .numbered - 1. [url #section-4 Типове на аритметични операции] - 2. [url #section-5 Лениво оценяване на булеви изрази] + 1. {uhref Типове на аритметични операции} + 2. {uhref Лениво оценяване на булеви изрази} -3. [url #undefined-behaviour Undefined behaviour] +3. {uhref Undefined behaviour} .numbered - 1. [url #section-6 Приоритет на скоби спрямо аритметични оператори] + 1. {uhref Приоритет на скоби спрямо аритметични оператори} -5. [url #section-7 Масиви и матрици] +5. {uhref Масиви и матрици} .numbered - 1. [url #section-8 Връщане на масиви] - 2. [url #section-9 Инициализация на масиви] - 3. [url #section-10 Масиви с неконстантен размер] - 4. [url #-int73-vs-int3-vs-int Тип на матрици (int[7\][3\] vs int(*)[3\] vs int**)] + 1. {uhref Връщане на масиви} + 2. {uhref Инициализация на масиви} + 3. {uhref Масиви с неконстантен размер} + 4. {uhref Тип на матрици (int[7][3] vs int(*)[3] vs int**)} @@ -48,24 +48,24 @@ Когато запишем булева стойност в променлива, лоша практика е да направим нещо такова: -```c++ +```cpp if (condition) myvar = true; else myvar = false; -Стойността на [`condition`] е булева сама по себе си, добрата практика е директно да запишем стойността: +Стойността на {` condition} е булева сама по себе си, добрата практика е директно да запишем стойността: -```c++ +```cpp myvar = condition; -Като [`condition`] е какъвто и да е булев израз. +Като {` condition} е какъвто и да е булев израз. -[$br2] +{br2} Еквивалентно имаме случая: -```c++ +```cpp bool f() { if (condition) return true; @@ -73,24 +73,24 @@ bool f() { return false; } -Коректното е просто да върнем [`condition`], тя е булева: +Коректното е просто да върнем {` condition}, тя е булева: -```c++ +```cpp bool f() { return condition; } -[$br2] +{br2} Трябва да е очевидно, но изрази от типа на: -```c++ +```cpp if (condition) myvar = false; else myvar = true; -```c++ +```cpp bool f() { if (condition) return false; @@ -101,10 +101,10 @@ bool f() { Спадат в същата категория. Резултата просто трябва да се обърне, булево: -```c++ +```cpp myvar = !condition; -```c++ +```cpp bool f() { return !condition; } @@ -117,7 +117,7 @@ bool f() { И върната стойност е от този тип. Например: -```c++ +```cpp int x = 5; double y = 6; std::cout << (x * y); // double е по-голямо от int, @@ -126,7 +126,7 @@ std::cout << (x * y); // double е по-голямо от int, Ефекта от това е, че деление на цели числа връща цяло число (което закръгля резултата). Например: -```c++ +```cpp int x = 5, y = 8; std::cout << (x / y); // деление на цели числа, резултата се закръгля надолу std::cout << (double)(x / y); // резултата от деление вече е закръглен, @@ -141,42 +141,42 @@ std::cout << ((double)x / y); // едното вече е с плаваща за Двата главни примера се срещат при оператори "и" и "или". -[$br2] +{br2} -```c++ +```cpp a || b || c || d -Ако [`a`] е истина, тогава другите няма да се изпълнят, защото тяхната стойност не е от значение, общия израз ще си остане истина. -Аналогично, ако [`a`] е лъжа, но [`b`] е истина, тогава [`c`] и [`d`] няма да се изпълнят. +Ако {` a} е истина, тогава другите няма да се изпълнят, защото тяхната стойност не е от значение, общия израз ще си остане истина. +Аналогично, ако {` a} е лъжа, но {` b} е истина, тогава {` c} и {` d} няма да се изпълнят. -[$br2] +{br2} -```c++ +```cpp a && b && c && d Напълно еквивалентно работи и "или". -Ако [`a`] е лъжа, тогава другите няма да се изпълнят, защото общия израз ще си остане лъжа. +Ако {` a} е лъжа, тогава другите няма да се изпълнят, защото общия израз ще си остане лъжа. -[$br2] +{br2} Това е релевантно когато използваме присвояващи оператори в логически условия, например: -```c++ +```cpp if (condition1 && ((myvar += 5) == 8)) и когато използваме функции: -```c++ +```cpp if (condition1 && f()) -И в двата случая, ако [`condition1`] е лъжа, тогава присвояването и извикването на функцията няма да се изпълнят. +И в двата случая, ако {` condition1} е лъжа, тогава присвояването и извикването на функцията няма да се изпълнят. Дали това е желан ефект или не зависи от кода. ## Undefined behaviour C++ наследява идеята от C, че някои неща в стандарта на езика не са дефинирани. -Тоест, какво ще се случи зависи от имплементацията на компилатора (това което превръща кода в [`.exe`]) и често се срещат разлики. +Тоест, какво ще се случи зависи от имплементацията на компилатора (това което превръща кода в {` .exe}) и често се срещат разлики. ### Приоритет на скоби спрямо аритметични оператори @@ -186,37 +186,37 @@ C++ наследява идеята от C, че някои неща в стан Този ефект е значим само при употреба на присвояващи оператори. Например: -```c++ +```cpp int x = 9; int y = (x += 5) * (x += 1) * (x -= 8); -Има два начина да се изчисли стойността на [`y`]: +Има два начина да се изчисли стойността на {` y}: :ordered 1. Първо изчисляваме всички изрази в скобите и след това изпълняваме умноженията. .p - Тоест, първо правим [`x += 5`], после [`x += 1`] и след това [`x -= 8`]. - Това променя стойността на [`x`] на 7 и стойността на [`y`] се свежда до [`x * x * x = 343`] + Тоест, първо правим {` x += 5}, после {` x += 1} и след това {` x -= 8}. + Това променя стойността на {` x} на 7 и стойността на {` y} се свежда до {` x * x * x = 343} 2. Първо изчисляваме умноженията. Това означава, че първо "слагаме" скоби около умноженията; израза се изпълнява все едно е: - ```c++ + ```cpp ((x += 5) * (x += 1)) * (x -= 8) .p - [*(Умножението е ляво асоциативно)*] + {i (Умножението е ляво асоциативно)} .p - Тоест, първо правим [`x += 5`] и след това [`x += 1`]. - Стойността на [`x`] е 15 и в големите скоби изчисляваме [`x * x = 225`]. + Тоест, първо правим {` x += 5} и след това {` x += 1}. + Стойността на {` x} е 15 и в големите скоби изчисляваме {` x * x = 225}. Сега израза се свежда до: - ```c++ + ```cpp 225 * (x -= 8) - Изпълнява се [`x -= 8`], [`x`] става 7, и [`y`] се свежда до [`225 * 7 = 1575`]. + Изпълнява се {` x -= 8}, {` x} става 7, и {` y} се свежда до {` 225 * 7 = 1575}. Първия метод се изпълнява от компилатора на Visual Studio, втория метод от gcc (много популярен C++ компилатор под Linux/MacOS). @@ -224,7 +224,7 @@ int y = (x += 5) * (x += 1) * (x -= 8); ## Масиви и матрици .important -Всички неща, които важат за статично-заделени масиви ([`int arr[5];`]), важат и за статично-заделени матрици ([`int mat[5][6];`])! +Всички неща, които важат за статично-заделени масиви ({` int arr[5];}), важат и за статично-заделени матрици ({` int mat[5][6];})! ### Връщане на масиви @@ -235,7 +235,7 @@ int y = (x += 5) * (x += 1) * (x -= 8); Например (да, в C++ можем да правим ей така обхвати): -```c++ +```cpp int main() { { int a = 5; // a се създава @@ -252,33 +252,33 @@ int main() { } // b се освобождава } -Като цяло, няма особено значение дали говорим за къдравите скоби на функция, на [`if`], на цикъл, ... +Като цяло, няма особено значение дали говорим за къдравите скоби на функция, на {` if}, на цикъл, ... -[$br2] +{br2} -Затова следното е [=грешно=]: +Затова следното е {b грешно}: -```c++ +```cpp int* f() { int arr[5] = { 1, 2, 3, 4, 5 }; return arr; } -[`arr`] е адрес в паметта (индекс, позиция, ..., която посочва клетка в паметта), някакво число. +{` arr} е адрес в паметта (индекс, позиция, ..., която посочва клетка в паметта), някакво число. -След като функцията свърши изпълнение и се върне стойността, ще сме "стигнали" къдравата скоба и [`arr`] ще бъде освободено. +След като функцията свърши изпълнение и се върне стойността, ще сме "стигнали" къдравата скоба и {` arr} ще бъде освободено. Може да зачитате, че стойностите се унищожават в този момент. Обаче, адреса сам по себе си е число, което си се връща и ние можем да работим с него както си искаме. -Тоест, [=C++ няма да ни се скара, че използваме масив, въпреки че той е, на практика, унищожен!=] +Тоест, {b C++ няма да ни се скара, че използваме масив, въпреки че той е, на практика, унищожен!} -[$br2] +{br2} -Ако искаме действително да върнем масив от стойности, трябва да го създадем в [=обхвата в който функцията се извиква и да го подадем като аргумент.=] +Ако искаме действително да върнем масив от стойности, трябва да го създадем в {b обхвата в който функцията се извиква и да го подадем като аргумент.} Например: -```c++ +```cpp void f(int* arr) { /* ... */ arr[0] = 28; @@ -295,111 +295,111 @@ int main() { Често използваме нотацията за къдрави скоби да инициализираме елементите на масив: -```c++ +```cpp int arr1[3] = { 1, 2, 3 }; Ако искаме да напълним масива с нули, често правим: -```c++ +```cpp int arr2[3] = { 0 }; -Обаче, ако примерно се опитаме да напълним масива с единици, тогава следното [=няма=] да проработи: +Обаче, ако примерно се опитаме да напълним масива с единици, тогава следното {b няма} да проработи: -```c++ +```cpp int arr3[3] = { 1 }; -[$br2] +{br2} Инициализацията чрез къдрави скоби работи по следния начин: всички елементи в масива се попълват спрямо подадените елементи в къдравите скоби. -[=Ако в къдравите скоби има по-малко елементи отколкото е голям масива, останалите елементи се попълват с нули!=] +{b Ако в къдравите скоби има по-малко елементи отколкото е голям масива, останалите елементи се попълват с нули!} -Тоест, в [`arr2`] ние попъляваме първия елемент да е нула, и останалите стават нули по подразбиране. -В [`arr3`] попълваме първия елемент да е единица и останалите стават нули по подразбиране. +Тоест, в {` arr2} ние попъляваме първия елемент да е нула, и останалите стават нули по подразбиране. +В {` arr3} попълваме първия елемент да е единица и останалите стават нули по подразбиране. Това навява на мисълта, дали не можем в C++ да пропуснем записването на първия елемент, когато искаме всичко да е нула? Можем! -```c++ +```cpp int arr4[3] = { }; // попълва arr4 с нули -[$br2] +{br2} Коректния метод, ако искаме да напълним масива с друг елемент, е да използваме цикъл. Например: -```c++ +```cpp for (int i = 0; i < 3; i++) arr[i] = 1; ### Масиви с неконстантен размер -[=Не=] позволяваме в този курс да правите такива неща: +{b Не} позволяваме в този курс да правите такива неща: -```c++ +```cpp int size; std::cin >> size; int arr[size]; -Въпреки че това [*може*] да се компилира и да работи. +Въпреки че това {i може} да се компилира и да работи. -Причината е защото този тип масиви се наричат [url https://en.wikipedia.org/wiki/Variable-length_array VLA]. -И в [=C++ VLA-ове не са позволени!=] +Причината е защото този тип масиви се наричат {url VLA;https://en.wikipedia.org/wiki/Variable-length_array}. +И в {b C++ VLA-ове не са позволени!} Въпреки това, много компилатори ги поддържат, главно защото много C++ компилатори са и C компилатори (и VLA-ове са позволени в C). -[=Единствения=] коректен метод да създадем масив, чиито размер не знаем предварително, е чрез динамича памет. +{b Единствения} коректен метод да създадем масив, чиито размер не знаем предварително, е чрез динамича памет. ### Тип на матрици (int[7][3] vs int(*)[3] vs int**) -```c++ +```cpp int mat[7][3]; -Какъв е типа на [`mat`]? +Какъв е типа на {` mat}? -От една страна е [`int[7][3]`]. -Но какъв е типа като указател, тоест когато няма въведени редове [=и=] колони? +От една страна е {` int[7][3]}. +Но какъв е типа като указател, тоест когато няма въведени редове {b и} колони? -Отговорите са два: [`int(*)[3]`] и [`int*`]. +Отговорите са два: {` int(*)[3]} и {` int*}. Ето защо. -[$br2] +{br2} Да си припомним как работят нормалните масиви. Когато декларираме следното: -```c++ +```cpp int arr[14]; -Тогава в паметта се [*използва*] (заделя) [=последователност=] от клетки, която побира 14 [`int`] стойности. +Тогава в паметта се {i използва} (заделя) {b последователност} от клетки, която побира 14 {` int} стойности. -Също да си припомним, че [`arr`] е указател към първата от тези клетки. +Също да си припомним, че {` arr} е указател към първата от тези клетки. -На кратко, това означава че [`arr`] е адрес (индекс, позиция, ..., число което посочва в коя клетка на паметта се намираме). +На кратко, това означава че {` arr} е адрес (индекс, позиция, ..., число което посочва в коя клетка на паметта се намираме). Финално припомняме, че индексирането в масив е същото като отместване на адреса. -Т.е. [`arr[6]`] е еквивалентно на [`*(arr + 6)`]. +Т.е. {` arr[6]} е еквивалентно на {` *(arr + 6)}. -На кратко, ние преместваме нашия адрес (индекс в паметта) напред с 6 позиции и с [`*`] дереферираме (прочитаме стойността на подадения адрес). +На кратко, ние преместваме нашия адрес (индекс в паметта) напред с 6 позиции и с {` *} дереферираме (прочитаме стойността на подадения адрес). -Това означава, че [=всички елементи в масива е нужно да бъдат поставени последователно в паметта=]. +Това означава, че {b всички елементи в масива е нужно да бъдат поставени последователно в паметта}. -[$br2] +{br2} -Двумерните масиви [=работят по същия начин!=] +Двумерните масиви {b работят по същия начин!} -В декларацията на [`mat`], в паметта се заделя последователност от клетки в паметта, равна на размера на матрицата. -Тоест [`7 * 3 = 21`] [=последователни=] [*позиции*] в паметта. +В декларацията на {` mat}, в паметта се заделя последователност от клетки в паметта, равна на размера на матрицата. +Тоест {` 7 * 3 = 21} {b последователни} {i позиции} в паметта. -Тогава, [`mat[2][1]`] на какво е еквивалентно? +Тогава, {` mat[2][1]} на какво е еквивалентно? -Отговорът е [`*(mat + 2 * 7 + 1)`]. +Отговорът е {` *(mat + 2 * 7 + 1)}. Нашата таблица е разположена като последователност от клетки, където първо имаме клетките от първи ред, след това от втория ред и так. нат. Например, при следната дефиниция на матрица: -```c++ +```cpp int nums[3][2] = { { 8, 92 }, { 20, 50 }, @@ -411,59 +411,59 @@ int nums[3][2] = { ``` 8 92 20 50 7 45 -Как извеждаме елемента [`nums[2][1]`]? -Това е последната стойност в матрицата, [`45`]. +Как извеждаме елемента {` nums[2][1]}? +Това е последната стойност в матрицата, {` 45}. Щом искаме трети ред (втори по индекс), значи трябва да пропуснем първите два реда. -Това са общо 4 числа, как го сметнахме? [`Два реда * брой колони на ред = 2 * 2`]. +Това са общо 4 числа, как го сметнахме? {` Два реда * брой колони на ред = 2 * 2}. Сега сме на последния ред, искаме втория елемент (първи по индекс), значи отиваме на следващия елемент. -Така показахме, че [`nums[2][1]`] е еквивалентно на [`*(nums + 2 * 2 + 1)`]. +Така показахме, че {` nums[2][1]} е еквивалентно на {` *(nums + 2 * 2 + 1)}. -[$br2] +{br2} -Това трябва да поражда въпрос: не можем ли да използваме [`nums`] като едномерен масив и да напишем [`nums[2 * 2 + 1]`]? +Това трябва да поражда въпрос: не можем ли да използваме {` nums} като едномерен масив и да напишем {` nums[2 * 2 + 1]}? Можем! -Затова е смислено да преобразуваме [`int[3][2]`] към [`int*`]. +Затова е смислено да преобразуваме {` int[3][2]} към {` int*}. Единствения проблем, е че употребата става неприятна. Например: -```c++ +```cpp void f(int* matrix) { /* ... */ matrix[2 * 2 + 1]; /* ... */ } -[$br2] +{br2} Забелязваме, че ако C++ знае броя колони, би трябвало да може да направи сам тази сметка с пропускането на редове. -От там се поражда [`int(*)[2]`] като смислено преобразуване на [`int[3][2]`]. +От там се поражда {` int(*)[2]} като смислено преобразуване на {` int[3][2]}. Това е най-обикновен указател, обаче му даваме броя колони (и с това, подсказваме че ще работим с матрица). С такъв тип указател можем да използваме синтаксиса с двете квадратни скоби. Например: -```c++ +```cpp void g(int (*matrix)[2]) { /* ... */ matrix[2][1]; /* ... */ } -[$br2] +{br2} Очевиден проблем, е че броя колони трябва да бъде константен (зададен в типа), ако искаме да ползваме двата чифта квадратни скоби. Няма ли начин да ги използваме с неконстантен брой колони? -Отговорът е [=не!=] +Отговорът е {b не!} -Изкушаващо е да използваме [`int**`], но този тип обозначава нещо друго: указател към указател. +Изкушаващо е да използваме {` int**}, но този тип обозначава нещо друго: указател към указател. -Ако имаме [`int** x`], то тогава [`x[2][1]`] [=няма=] да бъде еквивалентно на [`*(x + 2 * колони + 1)`]! -Ще бъде еквивалентно на [`*(*(x + 2) + 1)`]! +Ако имаме {` int** x}, то тогава {` x[2][1]} {b няма} да бъде еквивалентно на {` *(x + 2 * колони + 1)}! +Ще бъде еквивалентно на {` *(*(x + 2) + 1)}! Това защо е така ще разберем когато учим динамична памет. Тогава, матриците ни няма да бъдат последователност от елементи, ами ще бъдат последователност от (указатели към) масиви. |