# Капани в C++ - УПП, КН, 2025-2026 'define expected-reading 15 min 'define created 22 November 2025 'define edited 23 November 2025 [$pagenav] .warn Този документ ще бъде обновяван до края на курса. Моля, следете за промени! [$br2] :contents :numbered 1. [url #section-1 Лоши практики] .numbered 1. [url #section-2 Записване/връщане на булеви стойности] 2. [url #section-3 Оператори] .numbered 1. [url #section-4 Типове на аритметични операции] 2. [url #section-5 Лениво оценяване на булеви изрази] 3. [url #undefined-behaviour Undefined behaviour] .numbered 1. [url #section-6 Приоритет на скоби спрямо аритметични оператори] 5. [url #section-7 Масиви и матрици] .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**)] Тази страница описва кажи-речи всички странности и неприятности в 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 и няма закръгляне ## Лениво оценяване на булеви изрази Всички булеви изрази се оценяват (изпълняват) лениво (мързеливо). Това означава, че ако израза е в състояние, където не е нужно да проверим останалите условия, няма да ги проверим. Двата главни примера се срещат при оператори "и" и "или". [$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]` е лъжа, тогава присвояването и извикването на функцията няма да се изпълнят. Дали това е желан ефект или не зависи от кода. ## 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). ## Масиви и матрици .important Всички неща, които важат за статично-заделени масиви (`[int arr[5];]`), важат и за статично-заделени матрици (`[int mat[5][6];]`)! ### Връщане на масиви Всяка променлива, която е декларирана или дефинирана (без помощта на динамича памет) съществува в определен обхват (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 arr1[3] = { 1, 2, 3 }; Ако искаме да напълним масива с нули, често правим: ```c++ int arr2[3] = { 0 }; Обаче, ако примерно се опитаме да напълним масива с единици, тогава следното =[няма]= да проработи: ```c++ int arr3[3] = { 1 }; [$br2] Инициализацията чрез къдрави скоби работи по следния начин: всички елементи в масива се попълват спрямо подадените елементи в къдравите скоби. =[Ако в къдравите скоби има по-малко елементи отколкото е голям масива, останалите елементи се попълват с нули!]= Тоест, в `[arr2]` ние попъляваме първия елемент да е нула, и останалите стават нули по подразбиране. В `[arr3]` попълваме първия елемент да е единица и останалите стават нули по подразбиране. Това навява на мисълта, дали не можем в C++ да пропуснем записването на първия елемент, когато искаме всичко да е нула? Можем! ```c++ int arr4[3] = { }; // попълва arr4 с нули [$br2] Коректния метод, ако искаме да напълним масива с друг елемент, е да използваме цикъл. Например: ```c++ for (int i = 0; i < 3; i++) arr[i] = 1; ### Масиви с неконстантен размер =[Не]= позволяваме в този курс да правите такива неща: ```c++ int size; std::cin >> size; int arr[size]; Въпреки че това *[може]* да се компилира и да работи. Причината е защото този тип масиви се наричат [url https://en.wikipedia.org/wiki/Variable-length_array VLA]. И в =[C++ VLA-ове не са позволени!]= Въпреки това, много компилатори ги поддържат, главно защото много C++ компилатори са и C компилатори (и VLA-ове са позволени в C). =[Единствения]= коректен метод да създадем масив, чиито размер не знаем предварително, е чрез динамича памет. ### Тип на матрици (int[7][3] vs int(*)[3] vs int**) ```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)]`! Това защо е така ще разберем когато учим динамична памет. Тогава, матриците ни няма да бъдат последователност от елементи, ами ще бъдат последователност от (указатели към) масиви. Тоест всеки ред в матрицата ще може да се намира на напълно различни позиции в паметта.