mirror of
https://github.com/adambard/learnxinyminutes-docs.git
synced 2024-11-22 21:52:31 +03:00
861 lines
46 KiB
Markdown
861 lines
46 KiB
Markdown
---
|
||
language: C
|
||
filename: learnc-ua.c
|
||
contributors:
|
||
- ["Adam Bard", "http://adambard.com/"]
|
||
- ["Árpád Goretity", "http://twitter.com/H2CO3_iOS"]
|
||
- ["Jakub Trzebiatowski", "http://cbs.stgn.pl"]
|
||
- ["Marco Scannadinari", "https://marcoms.github.io"]
|
||
- ["Zachary Ferguson", "https://github.io/zfergus2"]
|
||
- ["himanshu", "https://github.com/himanshu81494"]
|
||
- ["Joshua Li", "https://github.com/JoshuaRLi"]
|
||
- ["Dragos B. Chirila", "https://github.com/dchirila"]
|
||
translators:
|
||
- ["AstiaSun", "https://github.com/AstiaSun"]
|
||
lang: uk-ua
|
||
---
|
||
|
||
О, C! Досі мова для сучасних обчислень у високопродуктивних продуктах.
|
||
|
||
C це імовірно найбільш низькорівнева мова, яку будуть використовувати більшість програмістів. Проте, вона компенсує це не тільки швидкістю виконання. Як тільки ви оціните її можливість ручного управління пам'яттю, С зможе відвести саме в ті місця, в які вам потрібно було потрапити.
|
||
|
||
> **Дещо про прапори компілятора**
|
||
>
|
||
> За замовчуванням, gcc та clang досить тихо інформують про попередження та помилки
|
||
> при компіляції, хоч це і може бути дуже корисною інформацією. Тому рекомендується
|
||
> використовувати більш вимогливий компілятор. Ось кілька рекомендацій:
|
||
>
|
||
> `-Wall -Wextra -Werror -O2 -std=c99 -pedantic`
|
||
>
|
||
> За інформацією про ці та інші прапори зверніться до головної сторінки man вашого
|
||
> компілятора C (наприклад, `man 1 gcc`) або ж просто заґуґліть.
|
||
|
||
|
||
```c
|
||
// Однорядкові коментарі починаються з //
|
||
// Проте вони з'явились тільки після С99.
|
||
|
||
/*
|
||
Багаторядкові коментарі мають такий вигляд. І працюють в C89.
|
||
*/
|
||
|
||
/*
|
||
Багаторядкові коментарі не можуть вкладатись один в одний.
|
||
/* Будьте обережними */ // коментар закінчується на цьому рядку...
|
||
*/ // ...а не на цьому!
|
||
|
||
// Константа: #define <keyword>
|
||
// Назви констант, як правило, пишуться великими літерами, проте це не вимога
|
||
#define DAYS_IN_YEAR 365
|
||
|
||
// Ще одним способом оголосити константи є перелічення констант.
|
||
// До речі, всі вирази мають закінчуватись крапкою з комою.
|
||
enum days {SUN = 1, MON, TUE, WED, THU, FRI, SAT};
|
||
// MON отримає значення 2 автоматично, TUE дорівнюватиме 3 і т.д.
|
||
|
||
// Імпортувати заголовки можна за допомогою #include
|
||
#include <stdlib.h>
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
// (Заголовки із стандартної бібліотеки С вказуються між <кутовими дужками>.)
|
||
// Щоб додати власні заголовки, потрібно використовувати "подвійні лапки"
|
||
// замість кутових:
|
||
//#include "my_header.h"
|
||
|
||
// Сигнатури функцій попередньо оголошуються в .h файлах або на початку .с файлів.
|
||
void function_1();
|
||
int function_2(void);
|
||
|
||
// Потрібно оголосити 'прототип функції' перед main(), реалізація функцій
|
||
// відбувається після функції main().
|
||
int add_two_ints(int x1, int x2); // прототип функції
|
||
// Варіант `int add_two_ints(int, int);` теж правильний (не потрібно називати
|
||
// аргументи). Рекомендується також називати аргументи в прототипі для
|
||
// кращого розуміння.
|
||
|
||
// Вхідною точкою програми є функція під назвою main. Вона повертає чисельний тип.
|
||
int main(void) {
|
||
// реалізація програми
|
||
}
|
||
|
||
// Аргументи командного рядка, вказані при запуску програми, також передаються
|
||
// у функцію main.
|
||
// argc - це кількість переданих аргументів
|
||
// argv — це масив масивів символів, що містить самі аргументи
|
||
// argv[0] - назва програми, argv[1] - перший аргумент, і т.д.
|
||
int main (int argc, char** argv)
|
||
{
|
||
// printf дозволяє вивести на екран значення, вивід - це форматований рядок,
|
||
// в даному випадку %d позначає чисельне значення, \n — це новий рядок
|
||
printf("%d\n", 0); // => Виводить 0
|
||
|
||
///////////////////////////////////////
|
||
// Типи
|
||
///////////////////////////////////////
|
||
|
||
// Всі змінні повинні бути оголошені на початку поточного блоку області видимості.
|
||
// В цьому коді вони оголошуються динамічно. С99-сумісні компілятори
|
||
// дозволяють оголошення близько до місця, де значення використовується.
|
||
|
||
// int (цілочисельний знаковий тип) зазвичай займає 4 байти
|
||
int x_int = 0;
|
||
|
||
// short (цілочисельний знаковий тип) зазвичай займає 2 байти
|
||
//
|
||
short x_short = 0;
|
||
|
||
// Символьний тип char гарантовано займає 1 байт
|
||
char x_char = 0;
|
||
char y_char = 'y'; // Символьні літерали позначаються ''
|
||
|
||
// long (цілочисельний знаковий тип) має розмір від 4 до 8 байтів; великі значення
|
||
// типу long гарантовано займають 8 байтів
|
||
long x_long = 0;
|
||
long long x_long_long = 0;
|
||
|
||
// Тип float - це зазвичай 32-бітове число з плаваючою крапкою
|
||
float x_float = 0.0f; // Суфікс 'f' позначає літерал з плаваючою крапкою
|
||
|
||
// Тип double - це зазвийчай 64-бітове число з плаваючою крапкою
|
||
double x_double = 0.0; // дійсне число без суфіксів має тип double
|
||
|
||
// Цілочисельні типи можуть не мати знаку (бути більше, або ж рівними нулю)
|
||
unsigned short ux_short;
|
||
unsigned int ux_int;
|
||
unsigned long long ux_long_long;
|
||
|
||
// Char всередині одинарних лапок інтерпретуються як числа в наборі
|
||
// символів комп'ютера.
|
||
'0'; // => 48 в таблиці ASCII.
|
||
'A'; // => 65 в таблиці ASCII.
|
||
|
||
// sizeof(T) повертає розмір змінної типу Т в байтах
|
||
// sizeof(obj) віддає розмір виразу (змінна, літерал, і т.п.)
|
||
printf("%zu\n", sizeof(int)); // => 4 (на більшості пристроїв з 4-байтним словом)
|
||
|
||
// Якщо аргумент оператора `sizeof` — це вираз, тоді його аргументи не оцінюються
|
||
// (крім масивів, розмір яких залежить від змінної).
|
||
// Значення, що повертається в цьому випадку, - це константа часу компіляції.
|
||
int a = 1;
|
||
// size_t - беззнаковий чисельний тип розміром щонайменше 2 байти, який
|
||
// використовується для відображення розміру об'єкта.
|
||
size_t size = sizeof(a++); // a++ не оцінюється
|
||
printf("sizeof(a++) = %zu where a = %d\n", size, a);
|
||
// Виводить "sizeof(a++) = 4 where a = 1" (на 32-бітній архітектурі)
|
||
|
||
// Масиви повинні бути проініціалізовані з конкретним розміром.
|
||
char my_char_array[20]; // Цей масив займає 1 * 20 = 20 байтів
|
||
int my_int_array[20]; // Цей масив займає 4 * 20 = 80 байтів
|
||
// (припускаючи 4-байтні числа)
|
||
|
||
// Таким чином можна проініціалізувати масив нулем:
|
||
char my_array[20] = {0};
|
||
// де "{0}" називається "ініціалізатором масиву".
|
||
|
||
// Зазначте, можна явно не оголошувати розмір масиву, ЯКЩО ви проініціалізуєте
|
||
// масив у тому ж рядку. Тому, наступне оголошення еквівалентне:
|
||
char my_array[] = {0};
|
||
// АЛЕ, потрібно визначити розмір масиву під час виконання, як тут:
|
||
size_t my_array_size = sizeof(my_array) / sizeof(my_array[0]);
|
||
|
||
// ПОПЕРЕДЖЕННЯ якщо ви вирішили використовувати даний підхід, потрібно
|
||
// визначити розмір **перед тим**, як ви почнете передавати масив у функцію
|
||
// (побачите дискусію пізніше). Масиви перетворюються на вказівники при
|
||
// передачі як аргументи у функцію, тому попереднє твердження буде видавати
|
||
// хибний результат всередині функції.
|
||
|
||
// Індексація по масиву така ж сама, як і в інших мовах програмування або,
|
||
// скоріше, як у інших с-подібних мовах.
|
||
my_array[0]; // => 0
|
||
|
||
// Масиви незмінні, це просто частина пам'яті!
|
||
my_array[1] = 2;
|
||
printf("%d\n", my_array[1]); // => 2
|
||
|
||
// Масиви, розмір яких залежить від змінної, в С99 (та в С11 як вибірковий
|
||
// функціонал) можуть бути оголошені також. Розмір такого масиву не має бути
|
||
// константою під час компіляції:
|
||
printf("Enter the array size: "); // спитати користувача розмір масиву
|
||
int array_size;
|
||
fscanf(stdin, "%d", &array_size);
|
||
int var_length_array[array_size]; // оголосити масив
|
||
printf("sizeof array = %zu\n", sizeof var_length_array);
|
||
|
||
// Приклад:
|
||
// > Enter the array size: 10
|
||
// > sizeof array = 40
|
||
|
||
// Рядки - це просто масиви символьних літералів (char), що закінчуються NULL
|
||
// (0x00) байтом, представленим у рядках як спеціальний символ '\0'.
|
||
// (Не потрібно включати байт NULL в рядкові літерали; компілятор сам вставляє
|
||
// його наприкінці масиву.)
|
||
char a_string[20] = "This is a string";
|
||
printf("%s\n", a_string); // %s форматує рядок
|
||
|
||
printf("%d\n", a_string[16]); // => 0
|
||
// тобто, байт #17 - це 0 (так само, як і 18-ий, 19-ий, та 20-ий)
|
||
|
||
// Якщо між одинарними лапками є букви, тоді це символьний літерал.
|
||
// Він має тип `int`, а не `char` (так історично склалось).
|
||
int cha = 'a'; // добре
|
||
char chb = 'a'; // також добре (неявне перетворення з int на char)
|
||
|
||
// Багатовимірні масиви:
|
||
int multi_array[2][5] = {
|
||
{1, 2, 3, 4, 5},
|
||
{6, 7, 8, 9, 0}
|
||
};
|
||
// Доступ до елементів:
|
||
int array_int = multi_array[0][2]; // => 3
|
||
|
||
///////////////////////////////////////
|
||
// Оператори
|
||
///////////////////////////////////////
|
||
|
||
// Скорочення для багатьох оголошень:
|
||
int i1 = 1, i2 = 2;
|
||
float f1 = 1.0, f2 = 2.0;
|
||
|
||
int b, c;
|
||
b = c = 0;
|
||
|
||
// Арифметичні операції
|
||
i1 + i2; // => 3
|
||
i2 - i1; // => 1
|
||
i2 * i1; // => 2
|
||
i1 / i2; // => 0 (0.5 округлено до 0)
|
||
|
||
// Потрібно перетворити хоча б одну з цілочисельних змінних на float, щоб
|
||
// отримати результат з плаваючою крапкою
|
||
(float)i1 / i2; // => 0.5f
|
||
i1 / (double)i2; // => 0.5 // Так само і для типу double
|
||
f1 / f2; // => 0.5, з певною точністю
|
||
// Такі обчислення не є точними
|
||
|
||
// Ділення за модулем також є
|
||
11 % 3; // => 2, остача від ділення
|
||
|
||
// Оператори порівняння ймовірно схожі, проте в С немає логічного типу.
|
||
// Натомість використовується int.
|
||
// (Або _Bool або bool в C99.)
|
||
// 0 - хибно (false), всі інші значення - правда (true). Оператори
|
||
// порівняння завжди повертають 0 або 1.
|
||
3 == 2; // => 0 (false)
|
||
3 != 2; // => 1 (true)
|
||
3 > 2; // => 1
|
||
3 < 2; // => 0
|
||
2 <= 2; // => 1
|
||
2 >= 2; // => 1
|
||
|
||
// C - це не Python, порівняння не утворюють ланцюги.
|
||
// Попередження: Рядок нижче скомпілюється, але він означає `(0 < a) < 2`.
|
||
// В даному випадку, це 1, тому що (0 < 1).
|
||
int between_0_and_2 = 0 < a < 2;
|
||
// Натомість потрібно використати:
|
||
int between_0_and_2 = 0 < a && a < 2;
|
||
|
||
// Логічні оператори з числами
|
||
!3; // => 0 (Логічне НЕ)
|
||
!0; // => 1
|
||
1 && 1; // => 1 (Логічне І)
|
||
0 && 1; // => 0
|
||
0 || 1; // => 1 (Логічне АБО)
|
||
0 || 0; // => 0
|
||
|
||
// Тернарний вираз з умовою ( ? : )
|
||
int e = 5;
|
||
int f = 10;
|
||
int z;
|
||
z = (e > f) ? e : f; // => 10 "if e > f return e, else return f."
|
||
|
||
// Оператори збільшення та зменшення на 1:
|
||
int j = 0;
|
||
int s = j++; // Повернути j ПОТІМ збільшити j. (s = 0, j = 1)
|
||
s = ++j; // Збільшити j ПОТІМ повернути j. (s = 2, j = 2)
|
||
// так само і для j-- та --j
|
||
|
||
// Побітові операції!
|
||
~0x0F; // => 0xFFFFFFF0 (побітове заперечення, "перше доповнення", результат
|
||
// для 32-бітного int)
|
||
0x0F & 0xF0; // => 0x00 (побітове І)
|
||
0x0F | 0xF0; // => 0xFF (побітове АБО)
|
||
0x04 ^ 0x0F; // => 0x0B (побітове XOR)
|
||
0x01 << 1; // => 0x02 (побітовий зсув вліво (на 1))
|
||
0x02 >> 1; // => 0x01 (побітовий зсув вправо (на 1))
|
||
|
||
// Будьте обережними при зсуві цілочисельних значень зі знаком.
|
||
// Наступні дії дають невизначений результат:
|
||
// - зсув на біт, що зберігає знак числа (int a = 1 << 31)
|
||
// - зсув вліво на від'ємне число (int a = -1 << 2)
|
||
// - зсув на число, що більше за ширину типу
|
||
// TODO: LHS
|
||
// - зсув на зміщення, що >= ширині типу в лівій частині виразу:
|
||
// int a = 1 << 32; // Невизначена поведінка, якщо ширина int 32 біти.
|
||
|
||
///////////////////////////////////////
|
||
// Структури розгалуження
|
||
///////////////////////////////////////
|
||
|
||
// Оператор умови
|
||
if (0) {
|
||
printf("I am never run\n"); // ніколи не буде виконано
|
||
} else if (0) {
|
||
printf("I am also never run\n"); // теж ніколи не буде виконано
|
||
} else {
|
||
printf("I print\n"); // це буде надруковано
|
||
}
|
||
|
||
// Цикл з передумовою
|
||
int ii = 0;
|
||
while (ii < 10) { // БУДЬ-ЯКЕ значення, що менше 10 - правда.
|
||
printf("%d, ", ii++); // ii++ збільшує ii на 1 ПІСЛЯ передачі поточного значення.
|
||
} // => надрукує "0, 1, 2, 3, 4, 5, 6, 7, 8, 9, "
|
||
|
||
printf("\n");
|
||
|
||
// Цикл з післяумовою
|
||
int kk = 0;
|
||
do {
|
||
printf("%d, ", kk);
|
||
} while (++kk < 10); // ++kk збільшує kk на 1 ПЕРЕД передачою поточного значення.
|
||
// => надрукує "0, 1, 2, 3, 4, 5, 6, 7, 8, 9, "
|
||
|
||
printf("\n");
|
||
|
||
// Цикл з лічильником
|
||
int jj;
|
||
for (jj=0; jj < 10; jj++) {
|
||
printf("%d, ", jj);
|
||
} // => виводить "0, 1, 2, 3, 4, 5, 6, 7, 8, 9, "
|
||
|
||
printf("\n");
|
||
|
||
// *****Додатково*****:
|
||
// Цикли та функції обов'язково повинні мати тіло. Якщо тіло не потрібно:
|
||
int i;
|
||
for (i = 0; i <= 5; i++) {
|
||
; // використовуйте крапку з комою, щоб симулювати тіло (пусте твердження)
|
||
}
|
||
// Або
|
||
for (i = 0; i <= 5; i++);
|
||
|
||
// Розгалуження з множинним вибором: switch()
|
||
switch (a) {
|
||
case 0: // значення повинні бути *константними* виразами і мати вбудований тип
|
||
//(наприклад, перелічення)
|
||
printf("Hey, 'a' equals 0!\n");
|
||
break; // якщо не використати break, то управління буде передано наступному блоку
|
||
case 1:
|
||
printf("Huh, 'a' equals 1!\n");
|
||
break;
|
||
// Будьте обережними, виконання продовжиться до тих пір, поки
|
||
// не зустрінеться наступний "break".
|
||
case 3:
|
||
case 4:
|
||
printf("Look at that.. 'a' is either 3, or 4\n");
|
||
break;
|
||
default:
|
||
// якщо вираз a не співпадає з описаними значеннями, то виконується
|
||
// блок default
|
||
fputs("Error!\n", stderr);
|
||
exit(-1);
|
||
break;
|
||
}
|
||
/*
|
||
Використання "goto" в С
|
||
*/
|
||
typedef enum { false, true } bool;
|
||
// вводимо таке перелічення, оскільки С не має логічного типу до С99
|
||
bool disaster = false;
|
||
int i, j;
|
||
for(i=0;i<100;++i)
|
||
for(j=0;j<100;++j)
|
||
{
|
||
if((i + j) >= 150)
|
||
disaster = true;
|
||
if(disaster)
|
||
goto error;
|
||
}
|
||
error :
|
||
printf("Error occurred at i = %d & j = %d.\n", i, j);
|
||
/*
|
||
https://ideone.com/GuPhd6
|
||
Даний приклад виведе "Error occurred at i = 51 & j = 99."
|
||
*/
|
||
|
||
///////////////////////////////////////
|
||
// Приведення до типів
|
||
///////////////////////////////////////
|
||
|
||
// Кожне значенння в С має тип, але можна перевести значення з одного типу в
|
||
// інший, якщо потрібно (із деякими обмеженнями).
|
||
|
||
int x_hex = 0x01; // Змінним можна присвоювати літерали в шістнадцятковій
|
||
// системі числення
|
||
|
||
// Приведення до типу призведе до спроби зберегти чисельне значення
|
||
printf("%d\n", x_hex); // => Виводить 1
|
||
printf("%d\n", (short) x_hex); // => Виводить 1
|
||
printf("%d\n", (char) x_hex); // => Виводить 1
|
||
|
||
// В данному випадку попередження не виникатиме, якщо значення виходить за межі
|
||
// значення типу
|
||
printf("%d\n", (unsigned char) 257); // => 1 (максимальне значення char = 255,
|
||
// якщо char має довжину 8 біт)
|
||
|
||
// Для того, щоб дізнатись максимальний розмір `char`, `signed char` або ж
|
||
// `unsigned char`, потрібно використати макроси CHAR_MAX, SCHAR_MAX та UCHAR_MAX
|
||
// відповідно з <limits.h>.
|
||
|
||
// Вбудовані типи можуть бути приведені до типу із плаваючою крапкою і навпаки.
|
||
printf("%f\n", (double) 100); // %f завжди перетворює число на double...
|
||
printf("%f\n", (float) 100); // ...навіть, якщо це float.
|
||
printf("%d\n", (char)100.0);
|
||
|
||
///////////////////////////////////////
|
||
// Вказівники
|
||
///////////////////////////////////////
|
||
|
||
// Вказівник - це змінна, що зберігає адресу у пам'яті. Оголошення вказівника
|
||
// також потребує інформації про тип об'єкта, на який він вказує. Можна
|
||
// отримати адресу пам'яті будь-якої змінної, а потім працювати з нею.
|
||
|
||
int x = 0;
|
||
printf("%p\n", (void *)&x); // Оператор & повертає адресу змінної у пам'яті
|
||
// (%p форматує об'єкт вказівника типу void *)
|
||
// => Виводить деяку адресу в пам'яті
|
||
|
||
// Для оголошення вказівника потрібно поставити * перед його назвою.
|
||
int *px, not_a_pointer; // px - це вказівник на цілочисельне значення (int)
|
||
px = &x; // Зберігає адресу змінної x в px
|
||
printf("%p\n", (void *)px); // => Виводить адресу в пам'яті
|
||
printf("%zu, %zu\n", sizeof(px), sizeof(not_a_pointer));
|
||
// => Виводить "8, 4" на звичайній 64-бітній системі
|
||
|
||
// Щоб прочитати значення, яке зберігається за адресою, на яку вказує вказівник,
|
||
// потрібно поставити знак * перед назвою змінної.
|
||
// Так, * використовується одночасно і для оголошення вказівника, і для отримання
|
||
// значення за адресою. Звучить заплутано, проте тільки спочатку.
|
||
printf("%d\n", *px); // => Виводить 0, значення x
|
||
|
||
// Можна також змінити значення, на яке посилається вказівник.
|
||
// Тут звернення до адреси обернене у круглі дужки, тому що
|
||
// ++ має вищий пріоритет виконання, ніж *.
|
||
(*px)++; // Збільшити значення, на яке вказує px, на 1
|
||
printf("%d\n", *px); // => Виводить 1
|
||
printf("%d\n", x); // => Виводить 1
|
||
|
||
// Масиви зручно використовувати для виділення неперервного блоку пам'яті.
|
||
int x_array[20]; // оголошує масив з 20 елементів (розмір можна задати лише один раз)
|
||
int xx;
|
||
for (xx = 0; xx < 20; xx++) {
|
||
x_array[xx] = 20 - xx;
|
||
} // Ініціалізує x_array значеннями 20, 19, 18,... 2, 1
|
||
|
||
// Оголосити вказівник типу int, який посилається на масив x_array
|
||
int* x_ptr = x_array;
|
||
// x_ptr тепер вказує на перший елемент масиву (число 20).
|
||
//
|
||
// Це працює, тому що при зверненні до імені масиву повертається вказівник
|
||
// на перший елемент. Наприклад, коли масив передається у функцію або присвоюється
|
||
// вказівнику, він неявно приводиться до вказівника.
|
||
// Виключення:
|
||
// - коли вказівник передається як аргумент із оператором `&`:
|
||
int arr[10];
|
||
int (*ptr_to_arr)[10] = &arr; // &arr НЕ має тип `int *`!
|
||
// Він має тип "вказівник на масив" (з 10 чисел).
|
||
// - коли масив - це рядковий літерал, що використовується для ініціалізації
|
||
// масив символів:
|
||
char otherarr[] = "foobarbazquirk";
|
||
// - коли масив - це аргумент операторів `sizeof` або `alignof`:
|
||
int arraythethird[10];
|
||
int *ptr = arraythethird; // те ж саме, що з int *ptr = &arr[0];
|
||
printf("%zu, %zu\n", sizeof(arraythethird), sizeof(ptr));
|
||
// Ймовірно, виводить "40, 4" або "40, 8"
|
||
|
||
// Інкрементація та декрементація вказівника залежить від його типу.
|
||
// (так звана арифметика вказівників)
|
||
printf("%d\n", *(x_ptr + 1)); // => Виводить 19
|
||
printf("%d\n", x_array[1]); // => Виводить 19
|
||
|
||
// Можна також динамічно виділити послідовні блоки в пам'яті за допомогою
|
||
// функції malloc зі стандартної бібліотеки. malloc приймає один аргумент типу
|
||
// size_t, що описує кількість байтів для виділення (зазвичай із купи, проте це
|
||
// може бути неправдою на вбудованих системах - стандарт С нічого про це не повідомляє).
|
||
int *my_ptr = malloc(sizeof(*my_ptr) * 20);
|
||
for (xx = 0; xx < 20; xx++) {
|
||
*(my_ptr + xx) = 20 - xx; // my_ptr[xx] = 20-xx
|
||
} // Проініціалізувати пам'ять значеннями 20, 19, 18, 17... 2, 1 (як int)
|
||
|
||
// Будьте обережними із передачею значень, що надаються користувачем, в malloc!
|
||
// Про всяк випадок, використовуйте calloc в таких ситуаціях (який, на відміну від
|
||
// malloc, також заповнює пам'ять нулями).
|
||
int* my_other_ptr = calloc(20, sizeof(int));
|
||
|
||
// Немає стандартного способу визначити розмір динамічно виділеного масиву в С.
|
||
// Через це, якщо масиви будуть часто передаватись в програмі, потрібна інша змінна,
|
||
// яка буде відслідковувати кількість елементів в масиві. Детальніше в розділі
|
||
// про функції.
|
||
size_t size = 10;
|
||
int *my_arr = calloc(size, sizeof(int));
|
||
// Додати елемент до масиву.
|
||
size++;
|
||
my_arr = realloc(my_arr, sizeof(int) * size);
|
||
if (my_arr == NULL) {
|
||
// Не забувайте перевіряти результат виконання realloc на помилки!
|
||
return
|
||
}
|
||
my_arr[10] = 5;
|
||
|
||
// Робота з вказівниками може призводити до неочікуваних і непрогнозованих
|
||
// результатів, якщо звернутись до пам'яті, що не була виділена вами.
|
||
printf("%d\n", *(my_ptr + 21)); // => Хто зна, що буде виведено.
|
||
// Може навіть вилетіти з помилкою.
|
||
|
||
// Після закінчення роботи із виділеною за допомогою malloc пам'яттю, її обов'язково
|
||
// потрібно звільнити. Інакше ніхто не зможе нею скористатися, аж поки програма не
|
||
// завершить свою роботу (така ситуація називається "витоком пам'яті").
|
||
free(my_ptr);
|
||
|
||
// Рядки - це масиви символів, проте вони найчастіше представлені як
|
||
// вказівник на символ (тобто, вказівник на перший елемент масиву). Вважається
|
||
// хорошим підходом використовувати `const char *', посилаючись на об'єкт
|
||
// рядка, оскільки його не можна змінити ("foo"[0] = 'a' ЗАБОРОНЕНО).
|
||
const char *my_str = "This is my very own string literal";
|
||
printf("%c\n", *my_str); // => 'T'
|
||
|
||
// Це не працюватиме, якщо рядок - це масив (потенційно створений за допомогою
|
||
// рядкового літерала), що зберігається у частині пам'яті, яку можна перезаписувати:
|
||
char foo[] = "foo";
|
||
foo[0] = 'a'; // Дозволяється, foo тепер містить "aoo"
|
||
|
||
function_1();
|
||
} // Кінець функції main
|
||
|
||
///////////////////////////////////////
|
||
// Функції
|
||
///////////////////////////////////////
|
||
|
||
// Синтаксис оголошення функції:
|
||
// <тип повернення> <назва функції>(<аргументи>)
|
||
|
||
int add_two_ints(int x1, int x2)
|
||
{
|
||
return x1 + x2; // Використовуйте return, щоб повернути значення
|
||
}
|
||
|
||
/*
|
||
Дані у функцію передають за значенням. Коли функція викликається, аргументи, що
|
||
передаються у функцію, копіюються з оригіналів (окрім масивів). Всі зміни над
|
||
значенням аргументів всередині функції не впливають на значення оригіналів.
|
||
|
||
Використовуйте вказівники, якщо потрібно редагувати безпосередньо оригінальні
|
||
значення аргументів.
|
||
|
||
Приклад: замінити рядок на обернений.
|
||
*/
|
||
|
||
// void означає, що функція нічого не повертає
|
||
void str_reverse(char *str_in)
|
||
{
|
||
char tmp;
|
||
size_t ii = 0;
|
||
size_t len = strlen(str_in); // `strlen()` це частина стандартної бібліотеки С
|
||
// Зауважте: довжина, яку повертає `strlen`, не включає
|
||
// термінальний NULL байт ('\0')
|
||
for (ii = 0; ii < len / 2; ii++) { // в C99 можна напряму оголошувати тип `ii` в циклі
|
||
tmp = str_in[ii];
|
||
str_in[ii] = str_in[len - ii - 1]; // ii-й символ з кінця
|
||
str_in[len - ii - 1] = tmp;
|
||
}
|
||
}
|
||
// Зауважте: для використання strlen() потрібно завантажити файл заголовку string.h
|
||
|
||
/*
|
||
char c[] = "This is a test.";
|
||
str_reverse(c);
|
||
printf("%s\n", c); // => ".tset a si sihT"
|
||
*/
|
||
/*
|
||
Оскільки можна повертати тільки одну змінну, для зміни значення більшої
|
||
кількості змінних можна використовувати виклик за посиланням
|
||
*/
|
||
void swapTwoNumbers(int *a, int *b)
|
||
{
|
||
int temp = *a;
|
||
*a = *b;
|
||
*b = temp;
|
||
}
|
||
/*
|
||
int first = 10;
|
||
int second = 20;
|
||
printf("first: %d\nsecond: %d\n", first, second);
|
||
swapTwoNumbers(&first, &second);
|
||
printf("first: %d\nsecond: %d\n", first, second);
|
||
// змінні обмінюються значеннями
|
||
*/
|
||
|
||
/*
|
||
Масиви завжди передаються у функції як вказівники, не зважаючи на тип масиву
|
||
(статичний чи динамічний). Тому всередині функція не знає про розмір масиву.
|
||
*/
|
||
// Розмір масиву завжди має передаватись разом із масивом!
|
||
void printIntArray(int *arr, size_t size) {
|
||
int i;
|
||
for (i = 0; i < size; i++) {
|
||
printf("arr[%d] is: %d\n", i, arr[i]);
|
||
}
|
||
}
|
||
/*
|
||
int my_arr[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
|
||
int size = 10;
|
||
printIntArray(my_arr, size);
|
||
// виведе "arr[0] is: 1" і т.д.
|
||
*/
|
||
|
||
// Ключове слово extern використовується, якщо всередині функції потрібно звернутись
|
||
// до змінної, що була оголошена поза функцією.
|
||
int i = 0;
|
||
void testFunc() {
|
||
extern int i; // використовуємо зовнішню змінну i
|
||
}
|
||
|
||
// Зробити зовнішню змінну приватною у вихідному файлі за допомогою static:
|
||
static int j = 0; // інші файли, що використовують testFunc2(),
|
||
// не матимуть доступу до змінної j
|
||
void testFunc2() {
|
||
extern int j;
|
||
}
|
||
// Ключове слово static робить змінну недоступною для коду поза даною одиницею
|
||
// компіляції. (На більшості систем, одиниця компіляції - це файл).
|
||
// static можна використовувати до глобальних змінних, функцій, локальних
|
||
// змінних у функціях. Локальні змінні, проініціалізовані static, поводять
|
||
// себе як глобальні змінні, проте тільки в межах даного файлу. Статичні
|
||
// змінні ініціалізуються 0, якщо інше значення не було вказане.
|
||
// **Як варіант, функції можна зробити приватними оголосивши їх як static**
|
||
|
||
///////////////////////////////////////
|
||
// Користувацькі типи та структури
|
||
///////////////////////////////////////
|
||
|
||
// Ключове слово typedef використовується, щоб створити псевдонім типу
|
||
typedef int my_type;
|
||
my_type my_type_var = 0;
|
||
|
||
// Структури - це такі собі колекції з даними. Пам'ять для полів виділяється
|
||
// послідовно, в порядку їх написання:
|
||
struct rectangle {
|
||
int width;
|
||
int height;
|
||
};
|
||
|
||
// Проте це не означає, що
|
||
// sizeof(struct rectangle) == sizeof(int) + sizeof(int)
|
||
// в зв'язку з вирівнюванням пам'яті [1]
|
||
|
||
void function_1()
|
||
{
|
||
struct rectangle my_rec;
|
||
|
||
// Доступ до полів структури відбувається через .
|
||
my_rec.width = 10;
|
||
my_rec.height = 20;
|
||
|
||
// Можна створити вказівники на структуру
|
||
struct rectangle *my_rec_ptr = &my_rec;
|
||
|
||
// Звернення до структури через вказівник та зміна значень поля:
|
||
(*my_rec_ptr).width = 30;
|
||
|
||
// Але є й альтернативний спосіб звернутись до поля через вказівник, використовуючи
|
||
// оператор -> (краще читається)
|
||
my_rec_ptr->height = 10; // Те ж саме, що (*my_rec_ptr).height = 10;
|
||
}
|
||
|
||
// Можна використати typedef перед struct
|
||
typedef struct rectangle rect;
|
||
|
||
int area(rect r)
|
||
{
|
||
return r.width * r.height;
|
||
}
|
||
|
||
// Якщо ваша структура доволі громіздка, можна звертатись до неї через вказівник,
|
||
// щоб уникнути копіювання всієї структури:
|
||
int areaptr(const rect *r)
|
||
{
|
||
return r->width * r->height;
|
||
}
|
||
|
||
///////////////////////////////////////
|
||
// Вказівники на функції
|
||
///////////////////////////////////////
|
||
/*
|
||
Під час виконання функції знаходяться за відомими адресами в пам'яті. Вказівники
|
||
на функції - це ті ж самі вказівники, що зберігають адресу у пам'яті, проте можуть
|
||
використовуватись, щоб викликати функції напряму і передавати обробники (або функції зі
|
||
зворотнім зв'язком). Хоча, синтаксис спочатку може бути доволі незрозумілим.
|
||
|
||
Приклад: use str_reverse from a pointer
|
||
*/
|
||
void str_reverse_through_pointer(char *str_in) {
|
||
// Оголосити вказівник на функцію під назвою f.
|
||
void (*f)(char *); // Сигнатура повинна точно співпадати із цільовою функцією.
|
||
f = &str_reverse; // Присвойте адресу певної функції (визначається під час виконання)
|
||
// f = str_reverse; повинно працювати також
|
||
(*f)(str_in); // Виклик функції через вказівник
|
||
// f(str_in); // Це альтернативний, але теж вірний синтаксис виклику функції.
|
||
}
|
||
|
||
/*
|
||
Якщо сигнатури функцій співпадають, можна присвоїти будь-яку функцію тому ж
|
||
самому вказівнику. Вказівники на функції зазвичай використовуються як псевдоніми
|
||
для спрощення та покращення читабельності коду. Приклад:
|
||
*/
|
||
|
||
typedef void (*my_fnp_type)(char *);
|
||
|
||
// Використання при оголошенні змінної вказівника:
|
||
// ...
|
||
// my_fnp_type f;
|
||
|
||
|
||
// Спеціальні символи:
|
||
/*
|
||
'\a'; // символ попередження (дзвінок)
|
||
'\n'; // символ нового рядка
|
||
'\t'; // символ табуляції (вирівнювання по лівому краю)
|
||
'\v'; // вертикальна табуляція
|
||
'\f'; // нова сторінка
|
||
'\r'; // повернення каретки
|
||
'\b'; // стирання останнього символу
|
||
'\0'; // нульовий символ. Зазвичай розташовується в кінці рядка.
|
||
// hello\n\0. \0 використовується для позначення кінця рядка.
|
||
'\\'; // зворотній слеш
|
||
'\?'; // знак питання
|
||
'\''; // одинарні лапки
|
||
'\"'; // подвійні лапки
|
||
'\xhh'; // шістнадцяткове число. Наприклад: '\xb' = символ вертикальної табуляції
|
||
'\0oo'; // вісімкове число. Наприклад: '\013' = символ вертикальної табуляції
|
||
|
||
// форматування виводу:
|
||
"%d"; // ціле число (int)
|
||
"%3d"; // ціле число, щонайменше 3 символи (вирівнювання по правому краю)
|
||
"%s"; // рядок
|
||
"%f"; // число з плаваючою крапкою (float)
|
||
"%ld"; // велике ціле число (long)
|
||
"%3.2f"; // число з плаваючою крапкою, щонайменше 3 цифри зліва і 2 цифри справа
|
||
"%7.4s"; // (аналогічно для рядків)
|
||
"%c"; // символ
|
||
"%p"; // вказівник. Зазначте: потребує перетворення типу на (void *) перед
|
||
// використанням у `printf`.
|
||
"%x"; // шістнадцяткове число
|
||
"%o"; // вісімкове число
|
||
"%%"; // друкує %
|
||
*/
|
||
|
||
///////////////////////////////////////
|
||
// Порядок виконання
|
||
///////////////////////////////////////
|
||
|
||
//---------------------------------------------------//
|
||
// Оператори | Асоціативність//
|
||
//---------------------------------------------------//
|
||
// () [] -> . | зліва направо //
|
||
// ! ~ ++ -- + = *(type)sizeof | справа наліво //
|
||
// * / % | зліва направо //
|
||
// + - | зліва направо //
|
||
// << >> | зліва направо //
|
||
// < <= > >= | зліва направо //
|
||
// == != | зліва направо //
|
||
// & | зліва направо //
|
||
// ^ | зліва направо //
|
||
// | | зліва направо //
|
||
// && | зліва направо //
|
||
// || | зліва направо //
|
||
// ?: | справа наліво //
|
||
// = += -= *= /= %= &= ^= |= <<= >>= | справа наліво //
|
||
// , | зліва направо //
|
||
//---------------------------------------------------//
|
||
|
||
/****************************** Файли заголовків *********************************
|
||
|
||
Файли заголовків важливі в С. Вони розділяють вихідний код та визначення на різні
|
||
файли, що робить їх кращими для розуміння.
|
||
|
||
Файли заголовків синтаксично подібні до вихідних файлів С, проте описуються у".h"
|
||
файлах. Їх можна додати в код за допомогою директиви #include "example.h", якщо
|
||
example.h існує в тому ж каталозі, що і файл С.
|
||
*/
|
||
|
||
/*
|
||
Так можна запобігти тому, що заголовок буде оголошений кілька разів. Така ситуація
|
||
виникає у випадку циклічної залежності, тобто коли вміст заголовку вже було
|
||
оголошено.
|
||
*/
|
||
#ifndef EXAMPLE_H /* якщо EXAMPLE_H ще не оголошено. */
|
||
#define EXAMPLE_H /* Визначити макрос EXAMPLE_H. */
|
||
|
||
/*
|
||
Заголовки можна додавати в інші заголовки, таким чином вони разом додаються
|
||
у подальшому.
|
||
*/
|
||
#include <string.h>
|
||
|
||
/*
|
||
Макроси можуть бути визначені також у заголовку та використовуватись у файлах,
|
||
що містять цей заголовок.
|
||
*/
|
||
#define EXAMPLE_NAME "Dennis Ritchie"
|
||
|
||
/* Макроси функції також можна визначити. */
|
||
#define ADD(a, b) ((a) + (b))
|
||
/*
|
||
Зверніть увагу на круглі дужки навколо аргументів! Важливо переконатись, що
|
||
a та b не можна проінтерпретувати інакше. Наприклад:
|
||
MUL(x, y) (x * y);
|
||
MUL(1 + 2, 3) -> (1 + 2 * 3), що є помилкою
|
||
*/
|
||
|
||
/* Struct та typedef можуть використовуватись для узгодженості між файлами. */
|
||
typedef struct Node
|
||
{
|
||
int val;
|
||
struct Node *next;
|
||
} Node;
|
||
|
||
/* Так само і перелічення. */
|
||
enum traffic_light_state {GREEN, YELLOW, RED};
|
||
|
||
/*
|
||
Прототипи функцій також можна оголосити так, щоб використовувати у кількох
|
||
файлах. Але так робити не варто. Краще оголосити їх у С файлі.
|
||
*/
|
||
Node createLinkedList(int *vals, int len);
|
||
|
||
/*
|
||
Окрім вище згаданих випадків, всі інші визначення мають описуватись у С файлах.
|
||
*/
|
||
|
||
#endif /* Кінець директиви передкомпіляції if. */
|
||
```
|
||
|
||
## Додаткові матеріали
|
||
|
||
Кращим посібником для вивчення С буде книга авторства Деніса Рітчі (творець С) та Браяна Кернігана,
|
||
[K&R, aka "The C Programming Language"](https://en.wikipedia.org/wiki/The_C_Programming_Language).
|
||
Але обережно з нею, книга старезна і містить неточності (ідеї, що вже вважаються не надто прийнятними).
|
||
|
||
Ще одним хорошим ресурсом є книга "Learn C The Hard Way" (наявна тільки англійською).
|
||
|
||
На деякі часті запитання дасть відповідь англомовний ресурс [compl.lang.c Frequently Asked Questions](http://c-faq.com).
|
||
|
||
Нагадаю, що важливо використовувати правильні інтервали, відступи та загалом мати узгоджений стиль коду.
|
||
Зручний для читання код краще, ніж складний код або зроблений нашвидкоруч. За прикладом можна звернутись до
|
||
[Linux kernel coding style](https://www.kernel.org/doc/Documentation/process/coding-style.rst).
|
||
|
||
Щодо всього іншого, Ґуґл на допомогу!
|
||
|
||
[1] [Чому розмір структури не дорівнює сумі розмірів її полів? (англ.)](http://stackoverflow.com/questions/119123/why-isnt-sizeof-for-a-struct-equal-to-the-sum-of-sizeof-of-each-member)
|