diff options
| Commit | 7abf7f19e11c74e54ae2c9b42b1b8ed2ba40eab7 (patch) | |
|---|---|---|
| Author | 2025-11-22 19:47:47 +0200 | |
| Committer | 2025-11-22 19:52:14 +0200 | |
| Files | 3b8b85e9d2785d476df8864f8911d47b373e69a0 | |
| syndamia.com-7abf7f19e11c74e54ae2c9b42b1b8ed2ba40eab7.tar syndamia.com-7abf7f19e11c74e54ae2c9b42b1b8ed2ba40eab7.tar.gz syndamia.com-7abf7f19e11c74e54ae2c9b42b1b8ed2ba40eab7.zip | ||
| Parent | 27fffba77f076db0bc80b2cd6ef8728022f2725b (diff) | |
feat(teaching/upp-2025): Add a page with C++ pitfalls
3 files changed, 405 insertions, 0 deletions
| -rw-r--r-- | meta/meta.tsv | 1 | ||||
| -rw-r--r-- | src/teaching/upp-2025/cpp-pitfalls/index.du | 401 | ||||
| -rw-r--r-- | src/teaching/upp-2025/index.du | 3 |
diff --git a/meta/meta.tsv b/meta/meta.tsv index 102c680..ce42c0e 100644 --- a/meta/meta.tsv +++ b/meta/meta.tsv @@ -88,6 +88,7 @@ teaching/oop-2023/week15/index.html Задачи - ООП, Седмица 15, 30 teaching/oop-2023/week16/index.html Задачи - ООП, Седмица 16, 06.06.2024 Условия на задачите за упражнение teaching/upp-2025/index.html Увод в програмирането - практикум, Компютърни науки, 5 група Материали на проведените през 2025/2026 практикуми по Увод в програмирането на Компютърни науки (2 поток), 5 група. +teaching/upp-2025/cpp-pitfalls/index.html Капани в C++ - УПП, КН, 2025-2026 Помощна страница с кажи-речи всички странности и неприятности, които трябва да се знаят за УП курса. teaching/upp-2025/week01/index.html Задачи - УПП, Седмица 1, 02.10.2025 Условия на задачите за упражнение. teaching/upp-2025/week01/concepts.html Общи концепции - УПП, Седмица 1, 02.10.2025 Общи концепции за компютрите, компилацията, git и GitHub Classroom. teaching/upp-2025/week01/github_classroom.html GitHub Classroom упътване - УПП, Седмица 1, 02.10.2025 Упътване как да си създадете GitHub акаунт и как да използвате GitHub Classroom. diff --git a/src/teaching/upp-2025/cpp-pitfalls/index.du b/src/teaching/upp-2025/cpp-pitfalls/index.du new file mode 100644 index 0000000..85bc5e2 --- /dev/null +++ b/src/teaching/upp-2025/cpp-pitfalls/index.du @@ -0,0 +1,401 @@ +# Капани в C++ - УПП, КН, 2025-2026 + +'define expected-reading 13 min +'define created 22 November 2025 +'define edited 22 November 2025 +[$pagenav] + +.warn +Този документ ще бъде обновяван до края на курса. +Добавки ще бъдат правени! + +[$br2] + +:contents +:numbered +1. [url #section-1 Лоши практики] + .numbered + 1. [url #section-2 Записване/връщане на булеви стойности] + +2. [url #section-3 Типове на аритметични операции] + +3. [url #undefined-behaviour Undefined behaviour] + .numbered + 1. [url #section-4 Приоритет на скоби спрямо аритметични оператори] + +4. [url #section-5 Лениво оценяване на булеви изрази] +5. [url #section-6 Връщане на масиви/матрици] +6. [url #section-7 Тип на матрици] + + + +Тази страница описва кажи-речи всички странности и неприятности в C++, който ще трябва да знаете в този курс. + +Постарал съм се да опиша всичко възможно най-подробно и просто. +Текста е много, но всичко е написано максимлано просто и детаилно. + +## Лоши практики + +Има някои типове изрази, които са коректен C++ код, но в този курс ще ги зачитаме за нещо некоректно. + +### Записване/връщане на булеви стойности + +Когато запишем булева стойност в променлива, лоша практика е да направим нещо такова: + +```c++ +if (condition) + myvar = true; +else + myvar = false; + +Стойността на `[condition]` е булева сама по себе си, добрата практика е директно да запишем стойността: + +```c++ +myvar = condition; + +Като `[condition]` е какъвто и да е булев израз. + +[$br2] + +Еквивалентно имаме случая: + +```c++ +bool f() { + if (condition) + return true; + else + return false; +} + +Коректното е просто да върнем `[condition]`, тя е булева: + +```c++ +bool f() { + return condition; +} + +[$br2] + +Трябва да е очевидно, но изрази от типа на: + +```c++ +if (condition) + myvar = false; +else + myvar = true; + +```c++ +bool f() { + if (condition) + return false; + else + return true; +} + +Спадат в същата категория. +Резултата просто трябва да се обърне, булево: + +```c++ +myvar = !condition; + +```c++ +bool f() { + return !condition; +} + +## Типове на аритметични операции + +При събиране, изваждане, умножение и деление, винаги имплицитно се конвертира към най-големия тип! +И върната стойност е от този тип. +Например: + +```c++ +int x = 5; +double y = 6; +std::cout << (x * y); // double е по-голямо от int, + // x се конверира и резултата е double + +Ефекта от това е, че деление на цели числа връща цяло число (което закръгля резултата). +Например: + +```c++ +int x = 5, y = 8; +std::cout << (x / y); // деление на цели числа, резултата се закръгля надолу +std::cout << (double)(x / y); // резултата от деление вече е закръглен, + // това преобразуване е твърде късно +std::cout << ((double)x / y); // едното вече е с плаваща запетая, + // резултата също става double и няма закръгляне + +## Undefined behaviour + +C++ наследява идеята от C, че някои неща в стандарта на езика не са дефинирани. +Тоест, какво ще се случи зависи от имплементацията на компилатора (това което превръща кода в `[.exe]`) и често се срещат разлики. + +### Приоритет на скоби спрямо аритметични оператори + +В C++ не е дефиниран приоритета на скобите спрямо аритметични операции. +Тоест, ако имаш дълъг аритметичен израз, не се определя дали първо ще се изпълнят всички неща в скобите и след това ще се изпълнят външните аритметични операции или обратно. + +Този ефект е значим само при употреба на присвояващи оператори. +Например: + +```c++ +int x = 9; +int y = (x += 5) * (x += 1) * (x -= 8); + +Има два начина да се изчисли стойността на `[y]`: + +:ordered +1. Първо изчисляваме всички изрази в скобите и след това изпълняваме умноженията. + + .p + Тоест, първо правим `[x += 5]`, после `[x += 1]` и след това `[x -= 8]`. + Това променя стойността на `[x]` на 7 и стойността на `[y]` се свежда до `[x * x * x = 343]` + +2. Първо изчисляваме умноженията. + Това означава, че първо "слагаме" скоби около умноженията; израза се изпълнява все едно е: + + ```c++ +((x += 5) * (x += 1)) * (x -= 8) + + .p + *[(Умножението е ляво асоциативно)]* + + .p + Тоест, първо правим `[x += 5]` и след това `[x += 1]`. + Стойността на `[x]` е 15 и в големите скоби изчисляваме `[x * x = 225]`. + Сега израза се свежда до: + + ```c++ +225 * (x -= 8) + + Изпълнява се `[x -= 8]`, `[x]` става 7, и `[y]` се свежда до `[225 * 7 = 1575]`. + + +Първия метод се изпълнява от компилатора на Visual Studio, втория метод от gcc (много популярен C++ компилатор под Linux/MacOS). + +## Лениво оценяване на булеви изрази + +Всички булеви изрази се оценяват (изпълняват) лениво (мързеливо). +Това означава, че ако израза е в състояние, където не е нужно да проверим останалите условия, няма да ги проверим. + +Двата главни примера се срещат при оператори "и" и "или". + +[$br2] + +```c++ +a || b || c || d + +Ако `[a]` е истина, тогава другите няма да се изпълнят, защото тяхната стойност не е от значение, общия израз ще си остане истина. +Аналогично, ако `[a]` е лъжа, но `[b]` е истина, тогава `[c]` и `[d]` няма да се изпълнят. + +[$br2] + +```c++ +a && b && c && d + +Напълно еквивалентно работи и "или". +Ако `[a]` е лъжа, тогава другите няма да се изпълнят, защото общия израз ще си остане лъжа. + +[$br2] + +Това е релевантно когато използваме присвояващи оператори в логически условия, например: + +```c++ +if (condition1 && ((myvar += 5) == 8)) + +и когато използваме функции: + +```c++ +if (condition1 && f()) + +И в двата случая, ако `[condition1]` е лъжа, тогава присвояването и извикването на функцията няма да се изпълнят. + +Дали това е желан ефект или не зависи от кода. + +## Връщане на масиви/матрици + +Всяка променлива, която е декларирана или дефинирана (без помощта на динамича памет) съществува в определен обхват (scope) и всички обхвати в него. + +Обхвати се определят с къдрави скоби. +Тоест, нивото на къдрави скоби определя къде и кога една променлива е налична. + +Например (да, в C++ можем да правим ей така обхвати): + +```c++ +int main() { + { + int a = 5; // a се създава + std::cout << 5; // a се изкарва + } // a се освобождава + std::cout << a; // грешка, a е унищожено + { + std::cout << a; // грешка, a e унищожено + int b = 6; // b се създава + std::cout << b; // b се изкарва + { + std::cout << b; // b се изкарва + } + } // b се освобождава +} + +Като цяло, няма особено значение дали говорим за къдравите скоби на функция, на `[if]`, на цикъл, ... + +[$br2] + +Затова следното е =[грешно]=: + +```c++ +int* f() { + int arr[5] = { 1, 2, 3, 4, 5 }; + return arr; +} + +`[arr]` е адрес в паметта (индекс, позиция, ..., която посочва клетка в паметта), някакво число. + +След като функцията свърши изпълнение и се върне стойността, ще сме "стигнали" къдравата скоба и `[arr]` ще бъде освободено. +Може да зачитате, че стойностите се унищожават в този момент. + +Обаче, адреса сам по себе си е число, което си се връща и ние можем да работим с него както си искаме. + +Тоест, =[C++ няма да ни се скара, че използваме масив, въпреки че той е, на практика, унищожен!]= + +[$br2] + +Ако искаме действително да върнем масив от стойности, трябва да го създадем в =[обхвата в който функцията се извиква и да го подадем като аргумент.]= +Например: + +```c++ +void f(int* arr) { + /* ... */ + arr[0] = 28; + /* ... */ +} +int main() { + /* ... */ + int arr[5]; + f(arr); + /* ... */ +} + +Всичко това важи и при матрици. + +## Тип на матрици + +```c++ +int mat[7][3]; + +Какъв е типа на `[mat]`? + +От една страна е `[int[7][3]]`. +Но какъв е типа като указател, тоест когато няма въведени редове =[и]= колони? + +Отговорите са два: `[int(*)[3]]` и `[int*]`. +Ето защо. + +[$br2] + +Да си припомним как работят нормалните масиви. +Когато декларираме следното: + +```c++ +int arr[14]; + +Тогава в паметта се *[използва]* (заделя) =[последователност]= от клетки, която побира 14 `[int]` стойности. + +Също да си припомним, че `[arr]` е указател към първата от тези клетки. + +На кратко, това означава че `[arr]` е адрес (индекс, позиция, ..., число което посочва в коя клетка на паметта се намираме). + +Финално припомняме, че индексирането в масив е същото като отместване на адреса. +Т.е. `[arr[6]]` е еквивалентно на `[*(arr + 6)]`. + +На кратко, ние преместваме нашия адрес (индекс в паметта) напред с 6 позиции и с `[*]` дереферираме (прочитаме стойността на подадения адрес). + +Това означава, че =[всички елементи в масива е нужно да бъдат поставени последователно в паметта]=. + +[$br2] + +Двумерните масиви =[работят по същия начин!]= + +В декларацията на `[mat]`, в паметта се заделя последователност от клетки в паметта, равна на размера на матрицата. +Тоест `[7 * 3 = 21]` =[последователни]= *[позиции]* в паметта. + +Тогава, `[mat[2][1]]` на какво е еквивалентно? + +Отговорът е `[*(mat + 2 * 7 + 1)]`. + +Нашата таблица е разположена като последователност от клетки, където първо имаме клетките от първи ред, след това от втория ред и так. нат. + +Например, при следната дефиниция на матрица: + +```c++ +int nums[3][2] = { + { 8, 92 }, + { 20, 50 }, + { 7, 45 } +}; + +В паметта имаме разположени така: + +``` +8 92 20 50 7 45 + +Как извеждаме елемента `[nums[2][1]]`? +Това е последната стойност в матрицата, `[45]`. + +Щом искаме трети ред (втори по индекс), значи трябва да пропуснем първите два реда. +Това са общо 4 числа, как го сметнахме? `[Два реда * брой колони на ред = 2 * 2]`. +Сега сме на последния ред, искаме втория елемент (първи по индекс), значи отиваме на следващия елемент. + +Така показахме, че `[nums[2][1]]` е еквивалентно на `[*(nums + 2 * 2 + 1)]`. + +[$br2] + +Това трябва да поражда въпрос: не можем ли да използваме `[nums]` като едномерен масив и да напишем `[nums[2 * 2 + 1]]`? + +Можем! +Затова е смислено да преобразуваме `[int[3][2]]` към `[int*]`. + +Единствения проблем, е че употребата става неприятна. +Например: + +```c++ +void f(int* matrix) { + /* ... */ + matrix[2 * 2 + 1]; + /* ... */ +} + +[$br2] + +Забелязваме, че ако C++ знае броя колони, би трябвало да може да направи сам тази сметка с пропускането на редове. +От там се поражда `[int(*)[2]]` като смислено преобразуване на `[int[3][2]]`. + +Това е най-обикновен указател, обаче му даваме броя колони (и с това, подсказваме че ще работим с матрица). +С такъв тип указател можем да използваме синтаксиса с двете квадратни скоби. +Например: + +```c++ +void g(int matrix(*)[2]) { + /* ... */ + matrix[2][1]; + /* ... */ +} + +[$br2] + +Очевиден проблем, е че броя колони трябва да бъде константен (зададен в типа), ако искаме да ползваме двата чифта квадратни скоби. + +Няма ли начин да ги използваме с неконстантен брой колони? +Отговорът е =[не!]= + +Изкушаващо е да използваме `[int**]`, но този тип обозначава нещо друго: указател към указател. + +Ако имаме `[int** x]`, то тогава `[x[2][1]]` =[няма]= да бъде еквивалентно на `[*(x + 2 * колони + 1)]`! +Ще бъде еквивалентно на `[*(*(x + 2) + 1)]`! + +Това защо е така ще разберем когато учим динамична памет. +Тогава, матриците ни няма да бъдат последователност от елементи, ами ще бъдат последователност от (указатели към) масиви. + +Тоест всеки ред в матрицата ще може да се намира на напълно различни позиции в паметта. diff --git a/src/teaching/upp-2025/index.du b/src/teaching/upp-2025/index.du index a4a811a..4fc9600 100644 --- a/src/teaching/upp-2025/index.du +++ b/src/teaching/upp-2025/index.du @@ -12,6 +12,9 @@ | [url https://github.com/18ivan18/IntroductionToProgramming2025 Материали от семинари] | [url https://learn.fmi.uni-sofia.bg/course/view.php?id=11601 Курс в мудъл] +=[Допълнителни ресурси:]= +[url ./cpp-pitfalls Капани в C++] + :table_3 |= № |= Дата |