From 6b8cae12cb5d60b37931332d9d8008649ae14566 Mon Sep 17 00:00:00 2001 From: Syndamia Date: Sat, 12 Sep 2026 11:31:19 +0300 Subject: feat!: Update dunbar markup Currently the newest version of the markup is not on the main branch, mostly because of polish and the documentation. As an effect from the changes to the language, the sitemap is the only thing which needs to be created externally. --- src/teaching/upp-2025/cpp-pitfalls/index.du | 240 ++++++++++++++-------------- 1 file changed, 120 insertions(+), 120 deletions(-) (limited to 'src/teaching/upp-2025/cpp-pitfalls') 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)}! Това защо е така ще разберем когато учим динамична памет. Тогава, матриците ни няма да бъдат последователност от елементи, ами ще бъдат последователност от (указатели към) масиви. -- cgit v1.3.1-26-gb76a