Сегодня начнём изучать код на C — языке, который лёг в основу большинства современных популярных языков, таких как Python, Java и C++. Сначала немного расскажем о том, как он появился, а потом про основной синтаксис C.
Это сложная тема, поэтому если вы только начинаете изучать программирование и хотите что-нибудь попроще, почитайте наш мастрид про Python или посмотрите, как быстро можно начать программировать на JavaScript.
А если вас не пугают сложности — добро пожаловать в мир крутого программирования.
Введение в C
C создал в 1972 году программист Деннис Ритчи. Язык можно назвать среднеуровневым языком программирования.
Языки высокого уровня более понятны человеку и более удобны для работы. Например, они могут сами следить за используемой памятью и разрешают менять типы переменных по ходу программы. Но зато они не такие гибкие и функциональные, как языки низкого уровня, где за всем нужно следить самому, — например, язык ассемблера.
C находится где-то посередине. На нём можно запрограммировать большинство вещей, которые могут прийти в голову, но во время работы нужно быть очень внимательным и понимать, что именно мы пишем и как это работает.
К тому же код на C более сложный, чем на Python или JavaScript. Самая простая программа с выводом Hello, world! потребует нескольких действий, которые не нужны в высокоуровневых языках:
// подключаем стандартную библиотеку для
// ввода/вывода, которая содержит функцию printf
#include <stdio.h>
// создаём основную функцию программы, точку входа,
// без неё программа не запустится
int main() {
// объявляем массив символов (строку)
// и инициализируем его значением "Hello, world!"
char program[] = "Hello, world!";
// выводим строку, которая хранится в массиве
// program, на экран, %s — спецификатор для строк
printf("%s\n", program);
// завершаем программу, возвращение 0 означает успешное завершение
return 0;
}
Обратите внимание, что даже для такой начальной задачи нам понадобился импорт стандартной библиотеки stdio.h
. Это ещё одно отличие от верхнеуровневых языков, где дополнительные модули будут полезны во многих случаях, но часто без них можно обойтись.
Основные элементы синтаксиса C
Вот основные категории, на которых строится код в языке C.
Комментарии — всё, что не относится к коду, но нужно для заметок и пояснения работы программы. Обозначаются двойной косой чертой //
:
// этот код программа проигнорирует
Комментарии могут быть многострочными, тогда они будут обозначаться как /**/
:
/*
этот комментарий
программа тоже проигнорирует
*/
Токены — наименьшие единицы кода, которые имеют значение для программы. Таких элементов 6 типов:
- идентификаторы;
- ключевые слова;
- операторы;
- строки;
- константы;
- специальные символы.
Ключевые слова — это зарезервированные слова языка, которые имеют специальное значение. Например, они могут обозначать тип данных, циклы или условие в программе.
Идентификаторы — имена переменных, функций и других объектов, которые создаёт программист. Они позволяют обращаться к данным и функциям по именам, поэтому важно придумывать идентификаторы с понятными названиями, которые помогают понять смысл работы кода.
В качестве идентификаторов нельзя использовать ключевые слова.
// age — идентификатор переменной
int age = 25;
// print_hello — идентификатор функции, которая выводит на экран слово Hello:
void print_hello() { printf("Hello\n"); }
Операторы — символы, которые выполняют определённые действия: присваивание, арифметические вычисления, логические сравнения:
// арифметический оператор сложения — «+»
int sum = 5 + 3;
// оператор сравнения — «==»
int isEqual = (5 == 3);
Все операторы:
Строки — последовательности символов в двойных кавычках: ""
. В C строки хранятся в виде массивов символов, заканчивающихся на \0
. Каждый символ занимает один байт, и символ \0
— тоже. Этого символа не видно при вводе, поэтому строка всегда занимает на 1 байт больше, чем есть символов внутри кавычек:
// В этой переменной хранятся слова
// "Hello, world!" и символ /0
char message[] = "Hello, world!";
Константы — неизменяемые значения в коде. Они бывают числовыми (10, 3.14) и символьными ('A').
На константах остановимся подробнее.
Чтобы объявить неизменяемые значения, можно использовать два ключевых слова:
const
. Ставится перед объявлением переменной, защищает её от изменения.#define
. Тоже ставится перед объявлением переменной. Но в скомпилированном коде этой переменной не существует, вместо неё сразу будет подставлено её значение. За счёт этого константа не занимает память.
Как это работает:
// создаём переменную в памяти
const int SIZE = 10;
// даём задание компилятору подставлять готовое
// значение везде, где он встретит константу SIZE
#define SIZE 10
Кроме констант, есть ещё одна как бы неизменяемая вещь — литералы. Это значения, которые записаны в коде. Например:
// литерал 10, целочисленный
int a = 10;
// литерал 3.14, вещественный
float b = 3.14;
// литерал 'A', символьный
char c = 'A';
// литерал "Hi" (строковый, включает \0)
char str[] = "Hi";
Литералы неизменяемы, но их переменные изменить можно ¯\_(ツ)_/¯
int a = 10;
// меняем значение:
a = 20;
Специальные символы используются для структурирования кода и использования операторов. Всего их 30 и к ним относятся разные скобки, математические символы, апострофы и несколько других. Про них ещё поговорим ниже.
Типы данных и переменные
Переменные — это один из основных блоков программы. В каждом языке правила работы с ними немного разные, хотя в целом все они похожи. Вот как это происходит в C.
Объявление переменных в C означает выделение памяти для хранения данных конкретного типа. Переменную нельзя использовать, пока она не объявлена.
Примеры объявления переменных:
// объявление переменной целочисленного типа
int age;
// объявление переменной для числа с плавающей точкой
float distance;
// объявление переменной для строки
char name;
В C есть правила, каким может быть имя переменной:
- оно должно начинаться с буквы или символа _;
- может содержать буквы, цифры и символ _;
- не может быть ключевым словом;
- зависит от регистра.
Инициализация переменных — это присвоение переменной начального значения. Можно делать это сразу при объявлении или позже:
// объявление и инициализация:
int age = 25;
float pi = 3.14;
char letter = 'A';
Примитивные типы данных — это основные типы, которые используются для хранения чисел, символов и логических значений. В С их 5:
char
для символов или строк. Пример —subscribe
.int
для целочисленного типа данных. Пример —221
.float
для десятичных значений, можно использовать до 6 цифр. Пример —10.123456
.double
используется для обозначения десятичных значений, допускается 15 цифр. Пример —10.111111111111111
.void
— пустые данные или отсутствие данных. Может использоваться в функциях для указания возвращаемого значения.
Структуры и объединения работают с несколькими переменными. Структура обозначается словом struct
и позволяет объединить несколько переменных разного типа в один объект. Объединения обозначаются как union
.
Работают оба типа похожим образом, но в объединениях все переменные находятся в одной области памяти и остаётся только последнее сохранённое значение.
Создание структуры:
struct Person {
char name[20];
int age;
};
Можно присваивать и изменять значения в обеих переменных, они сохранятся и не повлияют друг на друга.
Создание объединения выглядит так же:
union Data {
int i;
float f;
};
Переменные i
и f
делят одну ячейку памяти. Поэтому если мы сначала инициализируем i
, а потом f
, то компьютер сотрёт значение i
и сохранит вместо него f
.
Управляющие конструкции
Управляющие конструкции в C управляют порядком выполнения кода. Для этого в зависимости от условий выполняются или повторяются разные команды.
Условные операторы проверяют условия и выполняют подходящий результатам этой проверки код. Это операторы if
, else if
, else
.
Пример проверки на возраст:
int age = 18;
if (age >= 18) {
printf("Вы совершеннолетний!\n");
} else {
printf("Вам ещё не исполнилось 18\n");
}
Если age >= 18
, выполнится первый оператор вывода printf
, иначе — второй.
Циклы позволяют повторять код несколько раз. В С есть несколько вариантов реализации этой конструкции.
for
— цикл с параметрами. Используется, когда количество повторений конечно и это количество известно:
for (int i = 0; i < 5; i++) {
printf("i = %d\n", i);
}
Переменная i
увеличивается на 1 в каждой итерации, пока i < 5
.
while
— цикл с условием окончания. Если число повторений фрагмента кода неизвестно, можно установить завершение работы на какое-то условие.
Этот код выполняется, пока n > 0
:
int n = 3;
while (n > 0) {
printf("Осталось %d попыток\n", n);
n--;
}
do-while
— тоже цикл с условием окончания, но в любом случае выполняется минимум 1 раз, даже если условие изначально ложно. Посмотрите:
int x = 0;
do {
printf("x = %d\n", x);
x++;
} while (x < 3);
Код выполняется, пока x
меньше 3. Но первые строки кода выполняются до проверки условия после слова while
, поэтому даже если x
изначально больше 3, сначала на экран будет выведено значение переменной, а уже потом цикл остановится.
Переходные операторы управляют потоком выполнения внутри циклов и условий.
break
говорит машине закончить выполнение цикла:
for (int i = 0; i < 10; i++) {
// если i равен 5, цикл останавливается
if (i == 5) {
break;
}
printf("%d ", i);
}
continue
пропускает итерацию выполнения:
for (int i = 0; i < 5; i++) {
// если i = 2, цикл сразу переходит к следующей итерации
if (i == 2) {
continue;
}
printf("%d ", i);
}
goto
переносит выполнение кода в другое место программы. Сейчас использование этого оператора считается плохим тоном, поэтому он почти не применяется.
int x = 0;
start:
printf("x = %d\n", x);
x++;
// Пока x меньше 3, код будет возвращаться на строку start:
if (x < 3) goto start;
Функции
Функции позволяют разбивать код на блоки. Эти блоки можно вызывать одной командой, вместо того чтобы копировать большие фрагменты. Так повышается читаемость и удобство использования кода.
В C функцию нужно объявить перед её использованием. После этого её нужно определить, то есть описать логику. Пример:
#include <stdio.h>
// объявляем функцию
void sayHello();
// определяем функцию
void sayHello() {
printf("Привет, мир!\n");
}
int main() {
// вызываем функцию
sayHello();
return 0;
}
Здесь void sayHello();
— объявление, а void sayHello() {}
— определение.
Рекурсивная функция — вызов функции самой себя. Классический пример — функция по вычислению факториала.
Для вычисления факториала нужно перемножить все натуральные числа от 1 до этого числа. Функция для этого будет выглядеть так:
#include <stdio.h>
// функции факториала надо знать, от какого числа считать факториал
int factorial(int n) {
// факториал 0 равен 1, это базовый случай
if (n == 0) return 1;
// в остальных случаях n нужно умножить на функцию,
// в которую в качестве аргумента будет передано n - 1.
// Это будет продолжаться, пока n не будет равен 0
return n * factorial(n - 1);
}
int main() {
printf("Факториал 5: %d\n", factorial(5));
return 0;
}
При запуске в терминале получаем:
Факториал 5: 120
Массивы
Массивы и строки позволяют хранить несколько значений в одной переменной.
Массив — это набор элементов одного типа, расположенных в памяти подряд.
В массиве numbers 5
целых чисел:
int numbers[5] = {1, 2, 3, 4, 5};
Динамические массивы создаются во время работы программы. Это удобно, когда нам нужен переменный размер массива. Для их создания понадобится подключить ещё одну библиотеку, stdlib.h
. Без неё будут недоступны функции работы с динамическим выделением памяти:
// подключаем библиотеку для работы с вводом и выводом
#include <stdio.h>
// подключаем библиотеку для работы с памятью и её динамическим выделением
#include <stdlib.h>
// создаём основную функцию для запуска программы
int main() {
// выделяем память для 5 целых чисел
int *arr = (int *)malloc(5 * sizeof(int));
// создаём первый элемент массива: целое число 10
arr[0] = 10;
// освобождаем память
free(arr);
// завершаем работу функции
return 0;
}
Операторы
Операторы разберём подробнее, потому что в C использовать их нужно часто. Можно выделить три важные группы операторов.
Арифметические операторы нужны для математических операций над числами. Они позволяют выполнять стандартные операции: сложение, вычитание, умножение, деление, нахождение остатка от деления.
Список арифметических операторов:
+
, сложение;-
, вычитание;*
, умножение;/
, деление;%
, остаток от деления.
Логические операторы применяют для работы с булевыми выражениями, которые имеют значение «истина» или «ложь». Они возвращают результат, который также является булевым значением. Позволяют проверять несколько условий одновременно и приводить к одному комбинированному. Например, можно проверить, является ли число чётным и одновременно больше нуля.
Какие логические операторы есть в С:
&&
, логическое «И» (AND) — возвращаетtrue
, если оба условия истинны.||
, логическое «ИЛИ» (OR) — возвращаетtrue
, если хотя бы одно условие истинно.!
, логическое «НЕ» (NOT) — инвертирует булево значение условия. Например, != вернётtrue
, если значения не равны, то есть противоположность знаку=
.
Битовые операторы необходимы для работы с данными на уровне отдельных битов. Они полезны в системном программировании, работе с сетями, при реализации алгоритмов сжатия.
Битовые операторы:
&
, битовое «И» (AND) — выполняет операцию логического «И» по каждому биту.|
, битовое «ИЛИ» (OR) — выполняет операцию логического «ИЛИ» по каждому биту.^
, битовое исключающее «ИЛИ» (XOR) — выполняет операцию по каждому биту, возвращая 1, если биты разные.~
, побитовое «НЕ» (NOT) — инвертирует биты числа (меняет 1 на 0 и наоборот).<<
, сдвиг влево — сдвигает биты числа на заданное количество позиций влево.>>
, сдвиг вправо — сдвигает биты числа на заданное количество позиций вправо.
Инициализация и присваивание
Инициализация и присваивание переменных — основа процесса программирования, которая задаёт значения переменным.
Дизигнированные инициализаторы позволяют указывать значения элементов структуры или массива по их именам, а не по порядку.
Вот пример стандартного задания значений:
// создаём структуру с двумя элементами
struct Point {
int cooh;
int code;
};
// присваиваем значения полям по порядку
struct Point p = {5, 10};
А вот пример с дизигнированными инициализаторами:
// создаём структуру с двумя элементами
struct Point {
int cooh;
int code;
};
// указываем значения полей по именам
struct Point p = {.y = 10, .x = 5};
Комплексные литералы в C позволяют создать сложные данные, такие как массивы или структуры, без предварительного объявления их переменных. Их используют, когда нужно создать временные или одноразовые структуры данных. Это делает работу быстрее, а код — компактнее.
Например, так можно создать массив прямо в момент объявления:
int arr[] = {1, 2, 3, 4};
А вот пример с комплексным литералом для структуры:
struct Point p = {5, 10};
Что в следующий раз
Если вы уже немного знаете один из других языков, например Python, начинать работать на C уже не так страшно. Для следующей статьи возьмём какой-нибудь наш простой готовый проект на Питоне и перепишем его на Си. А потом сделаем ещё круче и создадим версию для C++.