Что ж, Си всё ещё является лидером среди современных высокопроизводительных языков.
Для большинства программистов, Си – это самый низкоуровневый язык на котором они когда-либо писали, но этот язык даёт больше, чем просто повышение производительности. Держите это руководство в памяти и вы сможете использовать Си максимально эффективно.
1// Однострочный комментарий начинается с // - доступен только после С99.
2
3/*
4Многострочный комментарий выглядит так. Работает начиная с С89.
5*/
6
7// Импорт файлов происходит с помощью **#include**
8#include <stdlib.h>
9#include <stdio.h>
10#include <string.h>
11
12// Файлы <в угловых скобочках> будут подключаться из стандартной библиотеки.
13// Свои файлы необходимо подключать с помощью "двойных кавычек".
14#include "my_header.h"
15
16// Объявление функций должно происходить в .h файлах или вверху .c файла.
17void function_1();
18void function_2();
19
20// Точка входа в программу – это функция main.
21int main() {
22 // для форматированного вывода в консоль используется printf
23 // %d – означает, что будем выводить целое число, \n переводит указатель вывода
24 // на новую строчку
25 printf("%d\n", 0); // => напечатает "0"
26 // Каждый оператор заканчивается точкой с запятой.
27
28 ///////////////////////////////////////
29 // Типы
30 ///////////////////////////////////////
31
32 // int обычно имеет длину 4 байта
33 int x_int = 0;
34
35 // short обычно имеет длину 2 байта
36 short x_short = 0;
37
38 // char гарантированно имеет длину 1 байта
39 char x_char = 0;
40 char y_char = 'y'; // Символьные литералы заключаются в кавычки ''
41
42 // long как правило занимает от 4 до 8 байт
43 // long long занимает как минимум 64 бита
44 long x_long = 0;
45 long long x_long_long = 0;
46
47 // float это 32-битное число с плавающей точкой
48 float x_float = 0.0;
49
50 // double это 64-битное число с плавающей точкой
51 double x_double = 0.0;
52
53 // Целые типы могут быть беззнаковыми.
54 unsigned short ux_short;
55 unsigned int ux_int;
56 unsigned long long ux_long_long;
57
58 // sizeof(T) возвращает размер переменной типа Т в байтах.
59 // sizeof(obj) возвращает размер объекта obj в байтах.
60 printf("%zu\n", sizeof(int)); // => 4 (на большинстве машин int занимает 4 байта)
61
62 // Если аргументом sizeof будет выражение, то этот аргумент вычисляется
63 // ещё во время компиляции кода (кроме динамических массивов).
64 int a = 1;
65 // size_t это беззнаковый целый тип который использует как минимум 2 байта
66 // для записи размера объекта
67 size_t size = sizeof(a++); // a++ не выполнится
68 printf("sizeof(a++) = %zu, где a = %d\n", size, a);
69 // выведет строку "sizeof(a++) = 4, где a = 1" (на 32-битной архитектуре)
70
71 // Можно задать размер массива при объявлении.
72 char my_char_array[20]; // Этот массив занимает 1 * 20 = 20 байт
73 int my_int_array[20]; // Этот массив занимает 4 * 20 = 80 байт (сумма 4-битных слов)
74
75 // Можно обнулить массив при объявлении.
76 char my_array[20] = {0};
77
78 // Индексация массива происходит также как и в других Си-подобных языках.
79 my_array[0]; // => 0
80
81 // Массивы изменяемы. Это просто память как и другие переменные.
82 my_array[1] = 2;
83 printf("%d\n", my_array[1]); // => 2
84
85 // В C99 (а также опционально в C11), массив может быть объявлен динамически.
86 // Размер массива не обязательно должен быть рассчитан на этапе компиляции.
87 printf("Enter the array size: "); // спрашиваем юзера размер массива
88 char buf[0x100];
89 fgets(buf, sizeof buf, stdin);
90 size_t size = strtoul(buf, NULL, 10); // strtoul парсит строку в беззнаковое целое
91 int var_length_array[size]; // объявление динамического массива
92 printf("sizeof array = %zu\n", sizeof var_length_array);
93 // Вывод программы (в зависимости от архитектуры) будет таким:
94 // > Enter the array size: 10
95 // > sizeof array = 40
96
97 // Строка – это просто массив символов, оканчивающийся нулевым (NUL (0x00)) байтом
98 // представляемым в строке специальным символом '\0'.
99 // Нам не нужно вставлять нулевой байт в строковой литерал,
100 // компилятор всё сделает за нас.
101 char a_string[20] = "This is a string";
102 printf("%s\n", a_string); // %s обозначает вывод строки
103
104 printf("%d\n", a_string[16]); // => 0
105 // байт #17 тоже равен 0 (а также 18, 19, и 20)
106
107 // Если между одинарными кавычками есть символ – это символьный литерал,
108 // но это тип int, а не char (по историческим причинам).
109
110 int cha = 'a'; // хорошо
111 char chb = 'a'; // тоже хорошо (подразумевается преобразование int в char)
112
113 ///////////////////////////////////////
114 // Операторы
115 ///////////////////////////////////////
116
117 // Можно использовать множественное объявление.
118 int i1 = 1, i2 = 2;
119 float f1 = 1.0, f2 = 2.0;
120
121 // Арифметика обычная
122 i1 + i2; // => 3
123 i2 - i1; // => 1
124 i2 * i1; // => 2
125 i1 / i2; // => 0 (0.5, но обрезается до 0)
126
127 f1 / f2; // => 0.5, плюс-минус погрешность потому что,
128 // цифры с плавающей точкой вычисляются неточно!
129
130 // Остаток от деления
131 11 % 3; // => 2
132
133 // Операции сравнения вам уже знакомы, но в Си нет булевого типа.
134 // Вместо него используется int. 0 это false, всё остальное это true.
135 // Операции сравнения всегда возвращают 1 или 0.
136 3 == 2; // => 0 (false)
137 3 != 2; // => 1 (true)
138 3 > 2; // => 1
139 3 < 2; // => 0
140 2 <= 2; // => 1
141 2 >= 2; // => 1
142
143 // Си это не Питон – операции сравнения могут быть только парными.
144 int a = 1;
145 // ОШИБКА:
146 int between_0_and_2 = 0 < a < 2;
147 // Правильно:
148 int between_0_and_2 = 0 < a && a < 2;
149
150 // Логика
151 !3; // => 0 (логическое НЕ)
152 !0; // => 1
153 1 && 1; // => 1 (логическое И)
154 0 && 1; // => 0
155 0 || 1; // => 1 (логическое ИЛИ)
156 0 || 0; // => 0
157
158 // Битовые операторы
159 ~0x0F; // => 0xF0 (побитовое отрицание)
160 0x0F & 0xF0; // => 0x00 (побитовое И)
161 0x0F | 0xF0; // => 0xFF (побитовое ИЛИ)
162 0x04 ^ 0x0F; // => 0x0B (исключающее ИЛИ (XOR))
163 0x01 << 1; // => 0x02 (побитовый сдвиг влево (на 1))
164 0x02 >> 1; // => 0x01 (побитовый сдвиг вправо (на 1))
165
166 // Будьте осторожны при сдвиге беззнакового int, эти операции не определены:
167 // - сдвиг в знаковый бит у целого числа (int a = 1 << 32)
168 // - сдвиг влево отрицательных чисел (int a = -1 << 2)
169
170 ///////////////////////////////////////
171 // Структуры ветвления
172 ///////////////////////////////////////
173
174 // Условный оператор
175 if (0) {
176 printf("I am never run\n");
177 } else if (0) {
178 printf("I am also never run\n");
179 } else {
180 printf("I print\n");
181 }
182
183 // Цикл с предусловием
184 int ii = 0;
185 while (ii < 10) {
186 printf("%d, ", ii++); // инкрементация происходит после того как
187 // значение ii передано ("postincrement")
188 } // => prints "0, 1, 2, 3, 4, 5, 6, 7, 8, 9, "
189
190 printf("\n");
191
192 // Цикл с постусловием
193 int kk = 0;
194 do {
195 printf("%d, ", kk);
196 } while (++kk < 10); // инкрементация происходит перед тем как
197 // передаётся значение kk ("preincrement")
198 // => prints "0, 1, 2, 3, 4, 5, 6, 7, 8, 9, "
199
200 printf("\n");
201
202 // Цикл со счётчиком
203 int jj;
204 for (jj=0; jj < 10; jj++) {
205 printf("%d, ", jj);
206 } // => prints "0, 1, 2, 3, 4, 5, 6, 7, 8, 9, "
207
208 printf("\n");
209
210 // Ветвление с множественным выбором
211 switch (some_integral_expression) {
212 case 0: // значения должны быть целыми константами (и могут быть выражениями)
213 do_stuff();
214 break; // если не написать break; то управление будет передано следующему блоку
215 case 1:
216 do_something_else();
217 break;
218 default:
219 // если не было совпадения, то выполняется блок default:
220 fputs("ошибка!\n", stderr);
221 exit(-1);
222 break;
223 }
224
225 ///////////////////////////////////////
226 // Форматирование вывода
227 ///////////////////////////////////////
228
229 // Каждое выражение в Си имеет тип, но вы можете привести один тип к другому,
230 // если хотите (с некоторыми искажениями).
231
232 int x_hex = 0x01; // Вы можете назначать переменные с помощью шестнадцатеричного кода.
233
234 // Приведение типов будет пытаться сохранять цифровые значения.
235 printf("%d\n", x_hex); // => Prints 1
236 printf("%d\n", (short) x_hex); // => Prints 1
237 printf("%d\n", (char) x_hex); // => Prints 1
238
239 // Типы могут переполняться без вызова предупреждения.
240 printf("%d\n", (unsigned char) 257); // => 1 (Max char = 255 if char is 8 bits long)
241
242 // Для определения максимального значения типов `char`, `signed char` и `unisigned char`,
243 // соответственно используйте CHAR_MAX, SCHAR_MAX и UCHAR_MAX макросы из <limits.h>
244
245 // Целые типы могут быть приведены к вещественным и наоборот.
246 printf("%f\n", (float)100); // %f formats a float
247 printf("%lf\n", (double)100); // %lf formats a double
248 printf("%d\n", (char)100.0);
249
250 ///////////////////////////////////////
251 // Указатели
252 ///////////////////////////////////////
253
254 // Указатель – это переменная которая хранит адрес в памяти.
255 // При объявлении указателя указывается тип данных переменной на которую он будет ссылаться.
256 // Вы можете получить адрес любой переменной, а потом работать с ним.
257
258 // Используйте & для получения адреса переменной.
259 int x = 0;
260 printf("%p\n", (void *)&x); // => Напечатает адрес в памяти, где лежит переменная x
261 // (%p выводит указатель на void *)
262
263 // Для объявления указателя нужно поставить * перед именем.
264 int *px, not_a_pointer; // px это указатель на int
265 px = &x; // сохранит адрес x в px
266 printf("%p\n", (void *)px); // => Напечатает адрес в памяти, где лежит переменная px
267 printf("%zu, %zu\n", sizeof(px), sizeof(not_a_pointer));
268 // => Напечатает "8, 4" в 64 битной системе
269
270 // Для того, чтобы получить значение по адресу, напечатайте * перед именем.
271 // Да, * используется при объявлении указателя и для получении значения по адресу
272 // немного запутано, но вы привыкнете.
273 printf("%d\n", *px); // => Напечатает 0, значение перемененной x
274
275 // Вы также можете изменять значение, на которое указывает указатель.
276 (*px)++; // Инкрементирует значение на которое указывает px на единицу
277 printf("%d\n", *px); // => Напечатает 1
278 printf("%d\n", x); // => Напечатает 1
279
280 // Массивы удобно использовать для большого количества однотипных данных.
281 int x_array[20];
282 int xx;
283 for (xx = 0; xx < 20; xx++) {
284 x_array[xx] = 20 - xx;
285 } // Объявление x_array с значениями 20, 19, 18,... 2, 1
286
287 // Объявление указателя на int с адресом массива.
288 int* x_ptr = x_array;
289 // x_ptr сейчас указывает на первый элемент массива (со значением 20).
290 // Это работает, потому что при обращении к имени массива возвращается
291 // указатель на первый элемент.
292 // Например, когда массив передаётся в функцию или присваивается указателю, он
293 // неявно преобразуется в указатель.
294 // Исключения: когда массив является аргументом для оператор '&':
295 int arr[10];
296 int (*ptr_to_arr)[10] = &arr; // &arr не является 'int *'!
297 // он является "указателем на массив" (из десяти 'int'ов).
298 // или когда массив это строчный литерал, используемый при объявлении массива символов:
299 char arr[] = "foobarbazquirk";
300 // или когда массив является аргументом `sizeof` или `alignof` операторов:
301 int arr[10];
302 int *ptr = arr; // то же самое что и "int *ptr = &arr[0];"
303 printf("%zu %zu\n", sizeof arr, sizeof ptr); // напечатает "40, 4" или "40, 8"
304
305 // Декрементация и инкрементация указателей зависит от их типа
306 // (это называется арифметика указателей)
307 printf("%d\n", *(x_ptr + 1)); // => Напечатает 19
308 printf("%d\n", x_array[1]); // => Напечатает 19
309
310 // Вы также можете динамически выделять несколько блоков памяти с помощью
311 // функции malloc из стандартной библиотеки, которая принимает один
312 // аргумент типа size_t – количество байт необходимых для выделения.
313 int *my_ptr = malloc(sizeof(*my_ptr) * 20);
314 for (xx = 0; xx < 20; xx++) {
315 *(my_ptr + xx) = 20 - xx; // my_ptr[xx] = 20-xx
316 } // Выделяет память для 20, 19, 18, 17... 2, 1 (как int'ы)
317
318 // Работа с памятью с помощью указателей может давать неожиданные и
319 // непредсказуемые результаты.
320 printf("%d\n", *(my_ptr + 21)); // => Напечатает кто-нибудь знает, что?
321 // Скорей всего программа вылетит.
322
323 // Когда вы закончили работать с памятью, которую ранее выделили, вам необходимо
324 // освободить её, иначе это может вызвать утечку памяти или ошибки.
325 free(my_ptr);
326
327 // Строки это массивы символов, но обычно они представляются как
328 // указатели на символ (как указатели на первый элемент массива).
329 // Хорошей практикой считается использование `const char *' при объявлении
330 // строчного литерала. При таком подходе литерал не может быть изменён.
331 // (например "foo"[0] = 'a' вызовет ошибку!)
332
333 const char *my_str = "This is my very own string literal";
334 printf("%c\n", *my_str); // => 'T'
335
336 // Это не работает, если строка является массивом
337 // (потенциально задаваемой с помощью строкового литерала)
338 // который находится в перезаписываемой части памяти:
339
340 char foo[] = "foo";
341 foo[0] = 'a'; // это выполнится и строка теперь "aoo"
342
343 void function_1()
344} // конец функции main()
345
346///////////////////////////////////////
347// Функции
348///////////////////////////////////////
349
350// Синтаксис объявления функции:
351// <возвращаемый тип> <имя функции>(аргументы)
352
353int add_two_ints(int x1, int x2) {
354 return x1 + x2; // Используйте return для возврата значения
355}
356
357/*
358Данные в функцию передаются "по значению", но никто не мешает
359вам передавать в функцию указатели и менять данные по указателям.
360
361Например: инвертировать строку прямо в функции
362*/
363
364// void означает, что функция ничего не возвращает
365void str_reverse(char *str_in) {
366 char tmp;
367 int ii = 0;
368 size_t len = strlen(str_in); // `strlen()` является частью стандартной библиотеки
369 for (ii = 0; ii < len / 2; ii++) {
370 tmp = str_in[ii];
371 str_in[ii] = str_in[len - ii - 1]; // ii-тый символ с конца
372 str_in[len - ii - 1] = tmp;
373 }
374}
375
376char c[] = "This is a test.";
377str_reverse(c);
378printf("%s\n", c); // => Выведет ".tset a si sihT"
379
380///////////////////////////////////////
381// Типы и структуры определяемые пользователем
382///////////////////////////////////////
383
384// typedef используется для задания стандартным типам своих названий
385typedef int my_type;
386my_type my_type_var = 0;
387
388// Структуры это просто коллекция данных, память выделяется последовательно,
389// в том порядке в котором записаны данные.
390struct rectangle {
391 int width;
392 int height;
393};
394
395// sizeof(struct rectangle) == sizeof(int) + sizeof(int) – не всегда верно
396// из-за особенностей компиляции (необычное поведение при отступах)[1].
397
398void function_1() {
399 struct rectangle my_rec;
400
401 // Доступ к структурам через точку
402 my_rec.width = 10;
403 my_rec.height = 20;
404
405 // Вы можете объявить указатель на структуру
406 struct rectangle *my_rec_ptr = &my_rec;
407
408 // Можно получить доступ к структуре и через указатель
409 (*my_rec_ptr).width = 30;
410
411 // ... или ещё лучше: используйте оператор -> для лучшей читабельности
412 my_rec_ptr->height = 10; // то же что и "(*my_rec_ptr).height = 10;"
413}
414
415// Вы можете применить typedef к структуре, для удобства.
416typedef struct rectangle rect;
417
418int area(rect r) {
419 return r.width * r.height;
420}
421
422// Если вы имеете большую структуру, можно получить доступ к ней "по указателю",
423// чтобы избежать копирования всей структуры.
424int area(const rect *r) {
425 return r->width * r->height;
426}
427
428///////////////////////////////////////
429// Указатели на функции
430///////////////////////////////////////
431
432/*
433Во время исполнения функции находятся по известным адресам в памяти.
434Указатель на функцию может быть использован для непосредственного вызова функции.
435Однако синтаксис может сбивать с толку.
436
437Пример: использование str_reverse по указателю
438*/
439
440void str_reverse_through_pointer(char *str_in) {
441 // Определение функции через указатель.
442 void (*f)(char *); // Сигнатура должна полностью совпадать с целевой функцией.
443 f = &str_reverse; // Присвоить фактический адрес (во время исполнения)
444 // "f = str_reverse;" тоже будет работать.
445 // Имя функции (как и массива) возвращает указатель на начало.
446 (*f)(str_in); // Просто вызываем функцию через указатель.
447 // "f(str_in);" или вот так
448}
На почитать ¶
Лучше всего найдите копию K&R, она же «The C Programming Language», это книга написанная создателями Си. Но будьте осторожны, она содержит идеи которые больше не считаются хорошими.
Если у вас появился вопрос, почитайте compl.lang.c Frequently Asked Questions.
Очень важно использовать правильные отступы и ставить пробелы в нужных местах. Читаемый код лучше, чем красивый или быстрый код. Чтобы научиться писать хороший код, почитайте Linux kernel coding style.