--- 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"] --- О, C! Досі мова для сучасних обчислень у високопродуктивних продуктах. C це імовірно найбільш низькорівнева мова, яку будуть використовувати більшість програмістів. Проте, вона компенсує це не тільки швидкістю виконання. Як тільки ви оціните її можливість ручного управління пам'яттю, С зможе відвести саме в ті місця, в які вам потрібно було потрапити. ```c // Однорядкові коментарі починаються з // // Проте вони з'явились тільки після С99. /* Багаторядкові коментарі мають такий вигляд. І працюють в C89. */ /* Багаторядкові коментарі не можуть вкладатись один в одний. /* Будьте обережними */ // коментар закінчується на цьому рядку... */ // ...а не на цьому! // Константа: #define // Назви констант, як правило, пишуться великими літерами, проте це не вимога #define DAYS_IN_YEAR 365 // Ще одним способом оголосити константи є перелічення констант. // До речі, всі вирази мають закінчуватись крапкою з комою. enum days {SUN = 1, MON, TUE, WED, THU, FRI, SAT}; // MON отримає значення 2 автоматично, TUE дорівнюватиме 3 і т.д. // Імпортувати заголовки можна за допомогою #include #include #include #include // (Заголовки із стандартної бібліотеки С вказуються між <кутовими дужками>.) // Щоб додати власні заголовки, потрібно використовувати "подвійні лапки" // замість кутових: //#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 // відповідно з . // Вбудовані типи можуть бути приведені до типу із плаваючою крапкою і навпаки. 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 /* Макроси можуть бути визначені також у заголовку та використовуватись у файлах, що містять цей заголовок. */ #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)