learnxinyminutes-docs/uk/c.md
2024-12-08 23:20:53 -07:00

45 KiB
Raw Permalink Blame History

filename contributors translators
learnc-ua.c
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
AstiaSun
https://github.com/AstiaSun

О, C! Досі мова для сучасних обчислень у високопродуктивних продуктах.

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". Але обережно з нею, книга старезна і містить неточності (ідеї, що вже вважаються не надто прийнятними).

Ще одним хорошим ресурсом є книга "Learn C The Hard Way" (наявна тільки англійською).

На деякі часті запитання дасть відповідь англомовний ресурс compl.lang.c Frequently Asked Questions.

Нагадаю, що важливо використовувати правильні інтервали, відступи та загалом мати узгоджений стиль коду. Зручний для читання код краще, ніж складний код або зроблений нашвидкоруч. За прикладом можна звернутись до Linux kernel coding style.

Щодо всього іншого, Ґуґл на допомогу!

[1] Чому розмір структури не дорівнює сумі розмірів її полів? (англ.)