mirror of
https://github.com/adambard/learnxinyminutes-docs.git
synced 2024-12-26 18:58:51 +00:00
861 lines
46 KiB
C
861 lines
46 KiB
C
|
---
|
|||
|
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)
|