Summary Files Commits Branches @commit @diff @stats
path: root / src / teaching / upp-2025 / cpp-pitfalls / index.du
blob: 09b9339652291d42aa140466c6776f39267d6245 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
'def title Капани в C++ - УПП, КН, 2025-2026
'def  desc Помощна страница с кажи-речи всички странности и неприятности, които трябва да се знаят за УП курса.

# {title}

{pagenav 15 min;22 November 2025;23 November 2025}

.warn
Този документ ще бъде обновяван до края на курса.
Моля, следете за промени!

{br2}

:contents
:numbered
1. {uhref Лоши практики}
   .numbered
   1. {uhref Записване/връщане на булеви стойности}

2. {uhref Оператори}
   .numbered
   1. {uhref Типове на аритметични операции}
   2. {uhref Лениво оценяване на булеви изрази}

3. {uhref Undefined behaviour}
   .numbered
   1. {uhref Приоритет на скоби спрямо аритметични оператори}

5. {uhref Масиви и матрици}
   .numbered
   1. {uhref Връщане на масиви}
   2. {uhref Инициализация на масиви}
   3. {uhref Масиви с неконстантен размер}
   4. {uhref Тип на матрици (int[7][3] vs int(*)[3] vs int**)}



Тази страница описва кажи-речи всички странности и неприятности в C++, който ще трябва да знаете в този курс.

Постарал съм се да опиша всичко възможно най-подробно и просто.
Текста е много, но всичко е написано максимлано просто и детаилно.

## Лоши практики

Има някои типове изрази, които са коректен C++ код, но в този курс ще ги зачитаме за нещо некоректно.

### Записване/връщане на булеви стойности

Когато запишем булева стойност в променлива, лоша практика е да направим нещо такова:

```cpp
if (condition)
    myvar = true;
else
    myvar = false;

Стойността на {` condition} е булева сама по себе си, добрата практика е директно да запишем стойността:

```cpp
myvar = condition;

Като {` condition} е какъвто и да е булев израз.

{br2}

Еквивалентно имаме случая:

```cpp
bool f() {
    if (condition)
        return true;
    else
        return false;
}

Коректното е просто да върнем {` condition}, тя е булева:

```cpp
bool f() {
    return condition;
}

{br2}

Трябва да е очевидно, но изрази от типа на:

```cpp
if (condition)
    myvar = false;
else
    myvar = true;

```cpp
bool f() {
    if (condition)
        return false;
    else
        return true;
}

Спадат в същата категория.
Резултата просто трябва да се обърне, булево:

```cpp
myvar = !condition;

```cpp
bool f() {
    return !condition;
}

## Оператори

### Типове на аритметични операции

При събиране, изваждане, умножение и деление, винаги имплицитно се конвертира към най-големия тип!
И върната стойност е от този тип.
Например:

```cpp
int x = 5;
double y = 6;
std::cout << (x * y); // double е по-голямо от int,
                      // x се конверира и резултата е double

Ефекта от това е, че деление на цели числа връща цяло число (което закръгля резултата).
Например:

```cpp
int x = 5, y = 8;
std::cout << (x / y);         // деление на цели числа, резултата се закръгля надолу
std::cout << (double)(x / y); // резултата от деление вече е закръглен,
                              // това преобразуване е твърде късно
std::cout << ((double)x / y); // едното вече е с плаваща запетая,
                              // резултата също става double и няма закръгляне

## Лениво оценяване на булеви изрази

Всички булеви изрази се оценяват (изпълняват) лениво (мързеливо).
Това означава, че ако израза е в състояние, където не е нужно да проверим останалите условия, няма да ги проверим.

Двата главни примера се срещат при оператори "и" и "или".

{br2}

```cpp
a || b || c || d

Ако {` a} е истина, тогава другите няма да се изпълнят, защото тяхната стойност не е от значение, общия израз ще си остане истина.
Аналогично, ако {` a} е лъжа, но {` b} е истина, тогава {` c} и {` d} няма да се изпълнят.

{br2}

```cpp
a && b && c && d

Напълно еквивалентно работи и "или".
Ако {` a} е лъжа, тогава другите няма да се изпълнят, защото общия израз ще си остане лъжа.

{br2}

Това е релевантно когато използваме присвояващи оператори в логически условия, например:

```cpp
if (condition1 && ((myvar += 5) == 8))

и когато използваме функции:

```cpp
if (condition1 && f())

И в двата случая, ако {` condition1} е лъжа, тогава присвояването и извикването на функцията няма да се изпълнят.

Дали това е желан ефект или не зависи от кода.

## Undefined behaviour

C++ наследява идеята от C, че някои неща в стандарта на езика не са дефинирани.
Тоест, какво ще се случи зависи от имплементацията на компилатора (това което превръща кода в {` .exe}) и често се срещат разлики.

### Приоритет на скоби спрямо аритметични оператори

В C++ не е дефиниран приоритета на скобите спрямо аритметични операции.
Тоест, ако имаш дълъг аритметичен израз, не се определя дали първо ще се изпълнят всички неща в скобите и след това ще се изпълнят външните аритметични операции или обратно.

Този ефект е значим само при употреба на присвояващи оператори.
Например:

```cpp
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. Първо изчисляваме умноженията.
   Това означава, че първо "слагаме" скоби около умноженията; израза се изпълнява все едно е:

   ```cpp
((x += 5) * (x += 1)) * (x -= 8)

   .p
   {i (Умножението е ляво асоциативно)}

   .p
   Тоест, първо правим {` x += 5} и след това {` x += 1}.
   Стойността на {` x} е 15 и в големите скоби изчисляваме {` x * x = 225}.
   Сега израза се свежда до:

   ```cpp
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++ можем да правим ей така обхвати):

```cpp
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}

Затова следното е {b грешно}:

```cpp
int* f() {
    int arr[5] = { 1, 2, 3, 4, 5 };
    return arr;
}

{` arr} е адрес в паметта (индекс, позиция, ..., която посочва клетка в паметта), някакво число.

След като функцията свърши изпълнение и се върне стойността, ще сме "стигнали" къдравата скоба и {` arr} ще бъде освободено.
Може да зачитате, че стойностите се унищожават в този момент.

Обаче, адреса сам по себе си е число, което си се връща и ние можем да работим с него както си искаме.

Тоест, {b C++ няма да ни се скара, че използваме масив, въпреки че той е, на практика, унищожен!}

{br2}

Ако искаме действително да върнем масив от стойности, трябва да го създадем в {b обхвата в който функцията се извиква и да го подадем като аргумент.}
Например:

```cpp
void f(int* arr) {
    /* ... */
    arr[0] = 28;
    /* ... */
}
int main() {
    /* ... */
    int arr[5];
    f(arr);
    /* ... */
}

### Инициализация на масиви

Често използваме нотацията за къдрави скоби да инициализираме елементите на масив:

```cpp
int arr1[3] = { 1, 2, 3 };

Ако искаме да напълним масива с нули, често правим:

```cpp
int arr2[3] = { 0 };

Обаче, ако примерно се опитаме да напълним масива с единици, тогава следното {b няма} да проработи:

```cpp
int arr3[3] = { 1 };

{br2}

Инициализацията чрез къдрави скоби работи по следния начин: всички елементи в масива се попълват спрямо подадените елементи в къдравите скоби.

{b Ако в къдравите скоби има по-малко елементи отколкото е голям масива, останалите елементи се попълват с нули!}

Тоест, в {` arr2} ние попъляваме първия елемент да е нула, и останалите стават нули по подразбиране.
В {` arr3} попълваме първия елемент да е единица и останалите стават нули по подразбиране.

Това навява на мисълта, дали не можем в C++ да пропуснем записването на първия елемент, когато искаме всичко да е нула?
Можем!

```cpp
int arr4[3] = { }; // попълва arr4 с нули

{br2}

Коректния метод, ако искаме да напълним масива с друг елемент, е да използваме цикъл.
Например:

```cpp
for (int i = 0; i < 3; i++)
    arr[i] = 1;

### Масиви с неконстантен размер

{b Не} позволяваме в този курс да правите такива неща:

```cpp
int size;
std::cin >> size;
int arr[size];

Въпреки че това {i може} да се компилира и да работи.

Причината е защото този тип масиви се наричат {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**)

```cpp
int mat[7][3];

Какъв е типа на {` mat}?

От една страна е {` int[7][3]}.
Но какъв е типа като указател, тоест когато няма въведени редове {b и} колони?

Отговорите са два: {` int(*)[3]} и {` int*}.
Ето защо.

{br2}

Да си припомним как работят нормалните масиви.
Когато декларираме следното:

```cpp
int arr[14];

Тогава в паметта се {i използва} (заделя) {b последователност} от клетки, която побира 14 {` int} стойности.

Също да си припомним, че {` arr} е указател към първата от тези клетки.

На кратко, това означава че {` arr} е адрес (индекс, позиция, ..., число което посочва в коя клетка на паметта се намираме).

Финално припомняме, че индексирането в масив е същото като отместване на адреса.
Т.е. {` arr[6]} е еквивалентно на {` *(arr + 6)}.

На кратко, ние преместваме нашия адрес (индекс в паметта) напред с 6 позиции и с {` *} дереферираме (прочитаме стойността на подадения адрес).

Това означава, че {b всички елементи в масива е нужно да бъдат поставени последователно в паметта}.

{br2}

Двумерните масиви {b работят по същия начин!}

В декларацията на {` mat}, в паметта се заделя последователност от клетки в паметта, равна на размера на матрицата.
Тоест {` 7 * 3 = 21} {b последователни} {i позиции} в паметта.

Тогава, {` mat[2][1]} на какво е еквивалентно?

Отговорът е {` *(mat + 2 * 7 + 1)}.

Нашата таблица е разположена като последователност от клетки, където първо имаме клетките от първи ред, след това от втория ред и так. нат.

Например, при следната дефиниция на матрица:

```cpp
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*}.

Единствения проблем, е че употребата става неприятна.
Например:

```cpp
void f(int* matrix) {
    /* ... */
    matrix[2 * 2 + 1];
    /* ... */
}

{br2}

Забелязваме, че ако C++ знае броя колони, би трябвало да може да направи сам тази сметка с пропускането на редове.
От там се поражда {` int(*)[2]} като смислено преобразуване на {` int[3][2]}.

Това е най-обикновен указател, обаче му даваме броя колони (и с това, подсказваме че ще работим с матрица).
С такъв тип указател можем да използваме синтаксиса с двете квадратни скоби.
Например:

```cpp
void g(int (*matrix)[2]) {
    /* ... */
    matrix[2][1];
    /* ... */
}

{br2}

Очевиден проблем, е че броя колони трябва да бъде константен (зададен в типа), ако искаме да ползваме двата чифта квадратни скоби.

Няма ли начин да ги използваме с неконстантен брой колони?
Отговорът е {b не!}

Изкушаващо е да използваме {` int**}, но този тип обозначава нещо друго: указател към указател.

Ако имаме {` int** x}, то тогава {` x[2][1]} {b няма} да бъде еквивалентно на {` *(x + 2 * колони + 1)}!
Ще бъде еквивалентно на {` *(*(x + 2) + 1)}!

Това защо е така ще разберем когато учим динамична памет.
Тогава, матриците ни няма да бъдат последователност от елементи, ами ще бъдат последователност от (указатели към) масиви.

Тоест всеки ред в матрицата ще може да се намира на напълно различни позиции в паметта.