2021-08-22 22:30:29 +03:00
---
2022-08-01 10:46:43 +03:00
language: C
2021-08-22 22:30:29 +03:00
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. */
```
2024-04-06 18:33:50 +03:00
2021-08-22 22:30:29 +03:00
## Додаткові матеріали
Кращим посібником для вивчення С буде книга авторства Деніса Рітчі (творець С ) та Браяна Кернігана,
[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 )