Summary Files Commits Branches @commit @diff @stats
path: root / src / teaching / upp-2025 / cpp-pitfalls / index.du
diff options
context:
space:
mode:
1 files changed, 120 insertions, 120 deletions
-rw-r--r--src/teaching/upp-2025/cpp-pitfalls/index.du240
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)}!
Това защо е така ще разберем когато учим динамична памет.
Тогава, матриците ни няма да бъдат последователност от елементи, ами ще бъдат последователност от (указатели към) масиви.