Основы синтаксиса языка C

Основы синтаксиса языка C

Начинаем программировать на одном из самых основных язык

Сегодня начнём изучать код на 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);

Все операторы:

Источник: geeksforgeeks.org

Строки — последовательности символов в двойных кавычках: "". В 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:

  1. char для символов или строк. Пример — subscribe.
  2. int для целочисленного типа данных. Пример — 221.
  3. float для десятичных значений, можно использовать до 6 цифр. Пример — 10.123456.
  4. double используется для обозначения десятичных значений, допускается 15 цифр. Пример — 10.111111111111111.
  5. 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++.

Обложка:

Алексей Сухов

Корректор:

Елена Грицун

Вёрстка:

Мария Климентьева

Соцсети:

Юлия Зубарева

Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Вам может быть интересно
hard
[anycomment]
Exit mobile version