Основы информатики и программирования.
План лекций

Содержание

Информатика и программирование

Информатика — наука о методах и процессах сбора, хранения, обработки, передачи, анализа и оценки информации, обеспечивающих возможность её использования для принятия решений.

В английском языке: "Computer Science" или "Computing Science" — наука о вычислениях (в общем абстрактном смысле, а не просто о компьютерах).

Программирование — создание компьютерной программы для выполнения определенных вычислений.

Парадигма программирования — совокупность идей и понятий, определяющих стиль написания компьютерных программ.

Мы будем в основном работать в императивной структурной парадигме программирование и использовать язык программирования Си.

Структура курса

Лекции

Лабораторные

  • практика программирования
  • требования
    • результат соответствует требованиям задачи
    • соблюдены сроки
    • код соответствует установленным стандартам
  • технические задания для каждой задачи сформулированы на странице курса
  • для каждой задачи определена стоимость в баллах
    • за каждую неделю просрочки стоимость уменьшается в два раза
  • успешная защита работы, если разработанная программа соответствует всем установленным требованиям

Самостоятельная работа

  • изучение материалов, документации, чтение дополнительной литературы
  • удаленная работа над заданиями

Практические

Зачет/экзамен

Вычислительная система ИМИТ

  • Система GNU/Linux, свободное ПО
  • Дисплейные классы
  • Терминальный сервер kappa (удаленная работа)
  • Домашние каталоги
  • Почта, чат, видеоконференции, веб-сервер, gitlab и др.

Техническая поддержка

  • support@cs.petrsu.ru
  • Вадим Анатольевич Пономарев (vadim@cs.petrsu.ru)
  • Михаил Александрович Крышень (kryshen@cs.petrsu.ru)

Язык программирования

  • элементарные выражения
  • средства комбинирования
  • средства абстракции

Значение ЯП, язык и ОС.

Язык Си

Первая программа (задание 1):

/**
 * main.c -- программа "Hello, students!"
 *
 * Copyright (c) 2022, Mikhail Kryshen <kryshen@cs.petrsu.ru>
 *
 * This code is licensed under MIT license.
 */

#include <stdio.h>

int main()
{
    /* Выводим приветствие */
    fprintf(stdout, "Hello, students!\n");

    return 0;
}

Этапы сборки программы

  • Препроцессор
  • Компиляция
  • Компоновка

Использование make

# цель по умолчанию (при вызове make или make task1)
# собираем программу task1 из объектного файла task1.o
task1: main.o
        gcc -g -O0 -o task1 main.o

main.o: main.c
        gcc -g -O0 -c main.c

# цель clean (при вызове make clean)
# удаляем программу и объектные файлы
clean:
        rm task1 *.o

Структура программы

  • Тело функции
    • операторы определения и описания переменных
    • операторы-выражения
      • непосредственные константы
      • переменные
      • операции
      • вызовы функций
    • операторы потока управления (структурное программирование)
      • ветвления
      • циклы
      • блочный (составной) оператор

Типы данных и ввод-вывод

#include <stdio.h>

#define PI 3.14159265358979323846

int main()
{
    /* Радиус круга */
    float r;

    /* Запрашиваем у пользователя радиус круга */
    fprintf(stdout, "Введите радиус: ");
    fscanf(stdin, "%f", &r);

    /* Вычисляем и печатаем площадь круга */
    fprintf(stdout, "Площаль круга: %.15f\n", PI * r * r);

    return 0;
}

Переменные в программе

  • модифицируемые объекты данных
  • идентифицируются именем
  • должны быть явно отнесены к некоторому типу

Имя переменной:

  • начинается с латинской буквы или символа подчеркивания
  • содержит латинские буквы, цифры или символ подчеркивания

Тип данных определяет

  • формат представления значений объекта данных
  • диапазон допустимых значений объекта данных
  • диапазон допустимых операций

Определение переменной: тип имя1, имя2…; тип имя = значение;

Описание переменной вводит имя (идентификатор) переменной и сообщает ее тип: компилятор получает возможность проверить корректность использования.

Неинициализированная переменная может иметь неопределенное значение.

Область видимости переменной — от описания до конца блока, в котором описана.

Целочисленные типы Формат scanf
char %hhd, %c (как символ)
short %hd
int %d
long %ld
С плавающей точкой Формат
float %f
double %lf
#include <stdio.h>

#define PI 3.14159265358979323846

int main()
{
    /* Радиус круга */
    float r;
    /* Площадь */
    float area;

    /* Запрашиваем у пользователя радиус круга */
    fprintf(stdout, "Введите радиус: ");
    fscanf(stdin, "%f", &r);

    /* Вычисляем и печатаем площадь круга */
    area = PI * r * r;
    fprintf(stdout, "Площаль круга: %.15f\n", area);

    return 0;
}

Почему второй пример выводит результат отличный от первого, например для r = 1?

Оператор-выражение

  • включает непосредственные константы, переменные, знаки операций и вызовы функций;
  • запись оканчивается точкой с запятой;
  • после выполнения управление передается следующему за ним оператору (стандартный поток управления).

Непосредственные константы:

  • Целочисленные: 101, 0777, 0xFF
  • Символьные: 'A', '\n', '\777', '\xFF'
  • С плавающей точкой: 1.0, 5.2E-2
  • Строковые: "Hello"

Операции:

  • Арифметика:
+ - * / %
  • Отношения:
< > <= >= == !=
  • Логические связки:
&& || !
  • Присваивания:
= += -= *= /= %=
  • Скобки: ()

В порядке приоритета:

1. !
2. * / %
3. + -
4. < > <= >=
5. == !=
6. &&
7. ||
8. = += -= *= /= %=

Присваивание:

L-value = expression;

  • L-value — «леводопустимое выражение»: определяет модифицируемый объект (доступную для записи область памяти), например, имя переменной.
  • Expression — «праводопустимое выражение»: допустимое синтаксисом языка выражение, тип которого совпадает с типом выражения в левой части или приводим к нему.
#include <stdio.h>

int main()
{
    int a = 10; /* Допустимая инициализация */
    int b = 20; /* Допустимая инициализация */

    /* Допустимые присваивания */
    a = 30;
    a = 2 * b + 1;
    a = a + 1;

    /* Недопустимые присваивания */
    1 = a;
    a + b = 40;

    return 0;
}

Операторы потока управления

if

/* Условие — выражение любого арифметического типа.
   Ненулевое значение — истина. */
if (условие) инструкция1 else инструкция2

/* Одиночный оператор завершается точкой с запятой, не разрывая if */
if (x > 0)
    y = x;
else
    y = -x;

/* Неполный оператор */
if (x > 0)
    y = x;

/* Использование операторных скобок */
if (x > 0) {
    y = x;
    z = y;
} else {
    y = -x;
    z = 0;
}

/* Каскад из нескольких if-else... */
if (a == 1) {
    printf("один\n");
} else {
    if (a == 2) {
        printf("два\n");
    } else {
        if (a == 3) {
            printf("три\n");
        } else {
            printf("много\n");
        }
    }
}

/* ...можно записать красивее, если убрать некоторые скобки... */
if (a == 1) {
    printf("один\n");
} else
    if (a == 2) {
        printf("два\n");
    } else
        if (a == 3) {
            printf("три\n");
        } else {
            printf("много\n");
        }

/* ...и некоторые переносы строк */
if (a == 1) {
    printf("один\n");
} else if (a == 2) {
    printf("два\n");
} else if (a == 3) {
    printf("три\n");
} else {
    printf("много\n");
}

/* Присваивание допускается в условиях */
if ((c = getchar()) != EOF) {
    fprintf(stdout, "Получен символ %c\n", c);
} else {
    fprintf(stderr, "Признак конца файла!\n");
}

/* Всегда истинно, надо было: x == 1 */
if (x = 1) ...

switch/case

Результат вычисления выражения последовательно сравнивается с константами в case, при совпадении выполнение начинается с инструкции после двоеточия (до конца switch или break).

В case допускаются константные выражения простых (скалярных) целочисленных типов: char, int, … В case не допускаются числа с плавающей точкой, составные объекты: массивы, строки

switch (выражение) {
case конст1 : инструкции
case конст2 : инструкции
...
default : инструкции
}

/* При x = 1 будут выведены строки 1 и 2. */
switch (x) {
case 1:
    fprintf(stdout, "1\n");
case 2:
    fprintf(stdout, "2\n");
    break;
case 3:
    fprintf(stdout, "3\n");
}

/* Можно группировать варианты. */
switch (x) {
case 1:
case 2:
    fprintf(stdout, "1+2\n");
    break;
case 3:
    fprintf(stdout, "3\n");
    break; /* не обязательно, но лучше оставить */
}

while

while (условие) тело
do тело while (условие)
/* Тело цикла — одна инструкция или блок */
while (x > 0)
    x = x — 10;

/* Или пустое */
while ((c = getchar()) != '\n');

/* Бесконечный цикл */
while (1) {
    ...
}

/* Вариант с постусловием: минимум одна итерация */
k = 0;
do
    k++;
while ((n = n / 10) != 0);

for

for (инициализация; выражение-условие; поствыражения)
s = 0;
i = 1;
while (i <= n) {
    s = s + i;
    i++;
}

/* Эквивалентно */
s = 0;
for (i = 1; i <= n; i++)
    s += i;

/* То же, но вариант выше лучше читается */
for (s = 0, i = 1; i <= n; s += i, i++);

break и continue

x = 0;
while (x < 10) {
    fprintf(stdout, "%d", x);
    x = x + 1;
    if (x == 3)
        break;
    fprintf(stdout, "!");
}

x = 0;
while (x < 10) {
    fprintf(stdout, "%d", x);
    x = x + 1;
    if (x >= 3)
        continue;
    fprintf(stdout, "!");
}

Тернарный оператор

условие ? выражение1 : выражение2

В отличие от if при вычислении получает определенное значение и может использоваться внутри других выражений.

int abs_a_minus_b = a > b ? a - b : b - a;

Функции

Подпрограмма (процедура, функция) — оформленный для целей повторного обращения именованный фрагмент кода программы.

  • базовый механизм абстракции
  • средство декомпозиции программы
  • избавление от повторов кода

Определение функции:

тип_возврата имя_функции(арг...)
{
    тело
}
  • void в качестве типа возвращаемого значения — функция ничего не возвращает,
  • void вместо списка аргументов — функция не принимает аргументы.

Дополнительные возможности:

  • вложенные определения (функция внутри функции)
  • функции с переменным числом аргументов (stdarg.h)

Пример:

/* Включает прототипы функций scanf и printf */
#include <stdio.h>

/* Прототип функции: функция должна быть объявлена до ее вызова. */
int max(int a, int b);

int main(void)
{
    int k, l, m;

    scanf("%d%d", &k, &l);

    /* Вызов функции: в качестве аргументов передаются текущие
       значения k и l, а m будет присвоено возвращенное значание. */
    m = max(k, l);

    printf("max(%d, %d) = %d\n", k, l, m);

    return 0;
}

int max(int a, int b)
{
    if (a > b)
        /* Завершить выполнение функции, вернув значение a. */
        return a;

    /* Это выполнится, только если не выполнено условие выше. */
    return b;
}

Следующие варианты реализации max() эквивалентны предыдущему:

int max(int a, int b)
{
    if (a > b)
        return a;
    else
        return b;
}
int max(int a, int b)
{
    /* Локальная переменная, не связана с 'k' в main(). */
    int k;

    if (a > b)
        k = a;
    else
        k = b;

    return k;
}
int max(int a, int b)
{
    return a > b ? a : b;
}
int max(int a, int b)
{
    if (a > b) {
        /* Присваиваем локальному 'b',
           значение 'l' в вызывающей main() не изменится. */
        b = a;
    }

    return b;
}

Рекурсия

Функция вызывает себя:

/* Возвращает факториал числа n. */
int factorial(int n)
{
    /* база рекурсии */
    if (n == 0) {
        return 1;
    }

    return n * factorial(n - 1);
}

= factorial(5) =
= 5 * factorial(4) =
= 5 * 4 * factorial(3) =

= 5 * 4 * 3 * 2 * 1 * factorial(0) =
= 5 * 4 * 3 * 2 * 1 * 1 =
= 120

"Глупое" суммирование:

int silly_add(int a, unsigned int b)
{
    if (b == 0) {
        return a;
    }

    return silly_add(a + 1, b - 1);
}

silly_add(5, 3) =
= silly_add(6, 2) =
= silly_add(7, 1) =
= silly_add(8, 0) =
= 8

Эквивалентно циклу:

int silly_add(int a, unsigned int b)
{
    while (b != 0) {
        a = a + 1;
        b = b - 1;
    }
    return a;
}

Почему программа завершается с ошибкой сегментирования при вызове рекурсивной версии silly_add с достаточно большим значением b (если откомпилировать без оптимизации)?

Стек вызовов

Стек — абстрактный тип данных, представляющий собой список элементов, организованных по принципу LIFO (last in — first out, последним пришел — первым вышел).

Операции со стеком:

  • push: добавить элемент,
  • pop: извлечь последний (находящийся на вершине стека) элемент.

stack.png

Аппаратный стек поддерживается центральным процессором и используется для хранения переменных и вызова подпрограмм.

Стек вызовов (call stack) — стек, хранящий информацию для возврата управления из подпрограмм (функций) в программу (или вызывающую функцию) при вложенных или рекурсивных вызовах.

Кадр стека выделяется при вызове функции и содержит связанное с ним состояние — адрес возврата, аргументы и локальные переменные. Структура кадра зависит от архитектуры процессора и соглашений системы (ABI).

#include <stdio.h>

int factorial(int n)
{
    if (n == 0) {
        return 1;
    }

    return n * factorial(n - 1);
}

int main(void)
{
    int a = 3;
    int b;

    b = factorial(a);

    return 0;
}
5 factorial(0) 1 →
4 factorial(1) 1 × 1 = 1 →
3 factorial(2) 2 × 1 = 2 →
2 factorial(3) 3 × 2 = 6 →
1 main(), переменные a и b 6 → b

Размер стека ограничен системой:

user@kappa:~> ulimit -s
8192

(ответ в килобайтах)

Переполнение стека приводит к аварийному завершению программы.

Объекты данных программы в оперативной памяти

Адресное пространство процесса

memory_layout.jpg

Данные размещаются в памяти. Объект данных простого типа может занимать несколько смежных байт оперативной памяти.

Адресом объекта является минимальный адрес среди байтов, принадлежащих объекту.

Адреса составляют виртуальное линейно упорядоченное пространство.

Числа, символы и другие объекты кодируются последовательностью бит.

Значения в памяти могут быть программно изменены, число различных значений \(2^k\), где \(k\) — общее число бит объекта данных.

Порядок байт

Байты объекта могут быть упорядочены в памяти разными способами: от старшего к младшему или наоборот.

0x12345678 — четырехбайтовое значение (шестнадцатеричное число равное десятичному 305419896). Размещено в памяти по адресу n.

Big Endian (тупоконечный порядок) — сначала старший байт:

n - 1 n n + 1 n + 2 n + 3 n + 4
    0x12 0x34 0x56 0x78    

Little Endian (остроконечный порядок) — сначала младший:

n - 1 n n + 1 n + 2 n + 3 n + 4
    0x78 0x56 0x34 0x12    

В архитектурах x86 и x86-64 используется остроконечный порядок.

Знаковые и беззнаковые целые

По умолчанию значения целого типа, кроме char, считается представленным в знаковой форме. Для char представление по умолчанию зависит от реализации. Использование знакового или беззнакового представления регулируется модификаторами signed и unsigned.

Для знаковых типов обычно используется дополнительный код (но стандарт допускает и другие способы представления).

signed char c = 127;   /* максимальное положительное,
                          битовое представление 0x7F (01111111) */

signed char c = -128;  /* минимальное отрицательное,
                          битовое представление 0x80 (10000000) */

signed char c = -1;    /* битовое представление 0xFF (11111111) */

unsigned char c = 255; /* 11111111 */

Модификаторы short и long варьируют размер базового целочисленного типа int. При этом int можно опускать:

int a; /* целое, размер определяется реализацией */
short int b; /* короткое целое, можно просто short */
long int c; /* длинное целое, можно просто long */
long long d; /* длинное целое повышенной емкости */
unsigned long e; /* можно комбинировать с unsigned */

Тип непосредственной константы задается суффиксами:

unsigned long a = 100lu;

Определяемые реализацией константы

#include <limits.h>
Имя 32 64 Гарантировано стандартом
CHAR_BIT 8 8  
CHAR_MIN -128 -128  
CHAR_MAX 127 127  
SCHAR_MIN -128 -128 -127
SCHAR_MAX 127 127 127
UCHAR_MAX 255 255 255
SHRT_MIN -32768 -32768 -32767
SHRT_MAX 32767 32767 32767
USHRT_MAX 65536 65536 65536
INT_MIN −2 147 483 648 −2 147 483 648 -32767
INT_MAX 2 147 483 647 2 147 483 647 32767
UINT_MAX 4 294 967 295 4 294 967 295 65535
LONG_MIN −2 147 483 648 -263 −2 147 483 647
LONG_MAX 2 147 483 647 263-1 2 147 483 647
ULONG_MAX 4 294 967 295 264 4 294 967 295
LLONG_MIN -263 -263 -(263-1)
LLONG_MAX 263-1 263-1 263-1
ULLONG_MAX 264 264 264

Файл limits.h содержит и другие константы, характеризующие реализацию языка и среду выполнения во время компиляции, например:

  • ARG_MAX — максимальный размер аргумента программы,
  • OPEN_MAX — максимальное количество открытых файлов,
  • PATH_MAX — максимальная длина пути файла.

Определение размера

Оператор sizeof:

/* размер типа int в байтах */
sizeof(int)

Назначение имен типов

typedef int image_size;
image_size width;
image_size height;

Целочисленные типы фиксированного размера

#include <stdint.h>

Заданный размер в битах:

  • int8_t
  • int16_t
  • int32_t
  • uint8_t
  • uint16_t
  • uint32_t

Дополнительно, если поддерживается реализацией:

  • int64_t
  • uint64_t

Минимальный тип не менее указанного размера: int_least8_t, int_least16_t, int_least32_t, int_least64_t, uint_least8_t, uint_least16_t, uint_least32_t, uint_least64_t.

Наиболее быстрый тип не менее указанного размера: int_fast8_t, int_fast16_t, int_fast32_t, int_fast64_t, uint_fast8_t, uint_fast16_t, uint_fast32_t, uint_fast64_t.

Типы максимального размера: intmax_t, uintmax_t.

И соответствующие константы _MIN и _MAX.

Типы для размеров

  • size_t — тип для размеров объектов данных,
  • ssize_t — аналогично, но может быть отрицательным для представления специальных значений.

Используются в функциях ввода-вывода (stdio.h), выделения памяти, работы со строками и др.

size_t strlen(const char *s);

Типы с плавающей запятой

\[m*b^e\]

IEEE 754-2008

  • нормализованная форма: \(a = s * 1,m * 2 ^ {(b - t)}\)
  • субнормали: \(a = s * 0,m * 2 ^ {-t}\)

Типы:

  • float — 32bit
  • double — 64bit
  • long double — 128bit

Специальные значения:

  • -Infinity, +Infinity
  • NaN

Десятичные дроби могут не иметь точного представления в двоичных форматах:
\(0,1_{10} = 0,0001100110011..._{2}\) (периодическая дробь).

Перечисления

enum тип_перечисления {список констант};

По умолчанию, первая константа получает значение 0, каждая последующая автоматически увеличивается на 1.

enum {
    ONE = 1,
    TWO,
    THREE
};

Массивы

Массивы в Си имеют целочисленные индексы, начинающиеся с нуля.

int a[10]; /* Одномерный массив на десять элементов
              a[0] ... a[9] */

int a[10][5]; /* Двумерный массив 10 строк по 5 элементов */

int a[3] = {1, 2, 3}; /* Определение и инициализация */

int a[] = {1, 2, 3}; /* Компилятор может и сам подсчитать */

int a[10] = {0}; /* Инициализация нулями */

int a[10] = {1}; /* Единица и 9 нулей */

/* Инициализация двумерного массива */
int a[3][2] = {{0, 1}, {2, 3}, {4, 5}};

Обращение к элементу по индексу: a[i]

Обработка массива размера N.

for (int i = 0; i < N; i++) {
    обработать a[i];
}

Обработка двумерного массива размера N×M:

for (int i = 0; i < N; i++) {
    for (int j = 0; j < M; j++) {
        обработать a[i][j];
    }
}

Размещение в памяти:

a[0][0] a[0][1] a[0][M-1] a[1][0] a[N-1][0] a[N-1][1] a[N-1][M-1]

Строки

Признаком конца строки является символ (байт) с кодом 0 ('\0') При выделении памяти под строку следует помнить про дополнительный байт для терминального нуля!

char str1[10]; /* Не более 9 полезных символов */

char str2[10] = "Hello!\n"; /* Сколько занято байт? */

char str3[] = "Hello!\n"; /* А так? */

char str4[] = {'H', 'e', 'l', 'l', 'o', '!', '\n', '\0'};

char str5[] = ""; /* Пустая строка — 1 байт */

Адреса и указатели

int x = 0;
  • x — имя переменной,
  • 0 — значение переменной,
  • &x — адрес переменной (указатель типа int*).

Можно вывести адрес:

printf("%p\n", &x);
char *pc;     /* указатель на объект типа char */
int *pn, *pm; /* указатели на объекты типа int */
void *ptr;    /* нетипизированный указатель */

NULL — специальное значение, «нулевой адрес», индикатор того, что указатель не адресует никакого объекта памяти Константа NULL определена в ряде стандартных заголовочных файлов. NULL эквивалентен численному значению 0, но не обязательно представлен последовательностью нулевых битов.

void *ptr = NULL;

Операция разыменовывания * позволяет обратиться к объекту по указателю. Не допускается разыменование void *.

int n;
int *pn, *pm;
pn = pm = &n; /* pn и pm указывают на n */

/* следующие операции эквивалентны */
n = 1;
*pn = 1;  /* присвоить значение по указателю pn */
*pm = 1;  /* pm также указывает на n */
  • &n — взятие адреса переменной,
  • *pn — доступ к объекту (переменной) по указателю.

Любой указатель приводим к void *, в остальных случаях необходимо явное приведение.

void *ptr;
float *px, *py;
ptr = px;
py = (float *) ptr;

Арифметика указателей

К указателю можно прибавить (вычесть) любое целое: переход через указанное число объектов данного типа в памяти.

int a[10];
int *p;

p = &a[0]; /* p указывает на начало массива */
p = p + 3; /* теперь p ссылается на a[3] */

Разность указателей может рассматриваться как расстояние между адресами выраженное в единицах кратных размеру типа. Для разности указателей предусмотрен тип ptrdiff_t (stddef.h).

int *p1 = &a[0];
int *p2 = &a[5];
ptrdiff_t k = p2 - p1; /* k = 5 */

Сравнение указателей:

/* следующие условия эквивалентны */
if (p != NULL) ...
if (p != 0) ...
if (p) ...

Указатели и массивы

Имя массива без индексов — константный указатель на тип элементов массива:

int x[] = {0, 2, 4, 6};
int *p;
p = &x[0]; /* адрес x[0] */
p = x;     /* то же */

Конструкция массив[индекс] эквивалентна *(массив + индекс):

int x[] = {0, 2, 4, 6};
int y;

/* эквивалентно */
y = x[2];
y = *(x + 2);
y = 2[x];

Аналогично, обращение к элементу многомерного массива: x[n][m][k] эквивалентно *(x[n][m] + k) и эквивалентно *(*(*(x + n) + m) + k).

Строки:

/* Массив s1 инициализирован значениями
   {'H', 'e', 'l', 'l', 'o', '!', '\0'}. */
char s1[] = "Hello!";

/* s2 ссылается на неизменяемую строку "Hello" в памяти. */
char *s2 = "Hello!";

s1[5] = '?'; /* строка s1 превратится в "Hello?" */
s2[5] = '?'; /* ошибка! */

Указатели в параметрах функции

Вычисление длины строки (эта функция доступна в string.h):

size_t strlen(const char *s) {
    size_t len = 0;
    while (s[len] != '\0') {
        len++;
    }
    return len;
}

const char *s — функция получает указатель на символ или массив символов и обязуется не изменять значения по этому указателю.

Что если терминального '\0' нет?

Еще одна реализация strlen:

size_t strlen(const char *s) {
    size_t len = 0;
    while (*s != '\0') {
        s++;
        len++;
    }
    return len;
}

И еще одна реализация:

size_t strlen(const char *s) {
    const char *p = s;
    /* ищем конец строки */
    while (*p != '\0')
        p++;
    /* длина строки — расстояние между концом и началом */
    return p - s;
}

Передача массива в функцию:

int sum_array (const int *a, int n)
{
    int s = 0;
    for (int i = 0; i < n; i++) {
        s += a[i];
    }
    return s;
}

Передача указателя в функцию для изменения значения:

void swap(int *a, int *b) {
    int temp;

    temp = *a;
    *a = *b;
    *b = temp;
}

А как тогда написать функцию swap для произвольного типа? Передать размер и обменивать побайтово:

void swap(void *a, void *b, size_t size) {
    char *p = (char *)a;
    char *q = (char *)b;
    char tmp;

    for (size_t i = 0; i < size; i++) {
        tmp = p[i];
        p[i] = q[i];
        q[i] = tmp;
    }
}

Или так, используя memcpy для копирования нужного количества байтов:

#include <string.h>

void swap(void *a, void *b, size_t size) {
    char tmp[size];

    memcpy(tmp, a, size); /* копирование size байт из a в tmp */
    memcpy(a, b, size);   /* из b в a */
    memcpy(b, tmp, size); /* из tmp в b */
}

int main(void) {
    int a = 1;
    int b = 2;
    swap(&a, &b, sizeof(int));

    return 0;
}

Или так (забегая вперед — динамическая память):

#include <stdlib.h>

void swap(void *a, void *b, size_t size) {
    void *tmp = malloc(size);

    memcpy(tmp, a, size); /* копирование size байт из a в tmp */
    memcpy(a, b, size);   /* из b в a */
    memcpy(b, tmp, size); /* из tmp в b */

    free(tmp);
}

Такая функция может и целые массивы обменивать значениями, но если a и b могут указывать на перекрывающиеся участки памяти, то нужно использовать еще один временный массив или memmove вместо memcpy.

Возврат массива из функции

int *natural_numbers(size_t n) {
    int nats[n];

    for (size_t i = 0; i < n; i++)
        nats[i] = i + 1;

    /* Опасно! Программа откомлилируется с предупреждением. */
    return nats;
}

Почему нельзя так делать?

А так можно, но вызывающая функция должна будет потом освободить выделенную под массив память:

#include <stdlib.h>

int *natural_numbers(size_t n) {
    /* Динамическое выделение памяти. */
    int *nats = malloc(n * sizeof(int));

    if (nats == NULL) {
        fprintf(stderr, "Недостаточно памяти.\n");
        exit(EXIT_FAILURE);
        /* Или можно было вернуть NULL, но тогда вызывающая функция
           должна обрабатывать ошибку. */
    }

    for (size_t i = 0; i < n; i++)
        nats[i] = i + 1;

    return nats;
}

Указатели на функции

Указатель на функцию может быть использован для вызова функции или передачи в функцию в качестве параметра.

/* Функция, возвращающая указатель на char. */
char *a (void);

/* Указатель на функцию, возвращающую char. */
char (*a) (void);

/* Указатель на функцию, возвращающую указатель на char. */
char *(*a) (void);

/* Массив из 10 указателей на функции, которые возвращают
   указатель на char. */
char *(*a[10]) (void);

Пример:

/* Включает прототипы функций scanf и printf */
#include <stdio.h>

/* Прототип функции: функция должна быть объявлена до ее вызова. */
int max(int a, int b);

int main(void)
{
    int (*f)(int a, int b);
    f = max;

    /* вызовет max по указателю f */
    int m = f(20, 10);
    /* или можно явно обозначить использование указателя:
       int m = (*f) (20, 10); */
    printf("%d\n", m);

    return 0;
}

int max(int a, int b)
{
    return a > b ? a : b;
}

Прототип универсальной функции сортировки из stdlib.h:

void qsort (void *base, size_t nmemb, size_t size,
            int (*compar)(const void *, const void *));

Можно сортировать любые объекты, для которых определена функция сравнения:

#include <stdio.h>
#include <stdlib.h>

int int_compar(const void *pa, const void *pb) {
    int a = *(const int *)pa;
    int b = *(const int *)pb;

    /* < 0: а перед b,
       > 0: b перед a,
       = 0: значения равны (порядок произвольный). */
    return a - b;
}

int main(void)
{
    int a[] = {5, 1, 2, -1, 2, -4, 10};

    qsort(a, 7, sizeof(int), int_compar);

    for (int i = 0; i < 7; i++) {
        printf("%d\n", a[i]);
    }    

    return 0;
}

Модули и заголовочные файлы

Задача: проверка "счастливого" билета. Номер билета из 6 цифр. Если сумма первых трех цифр равна сумме последних трех цифр, то билет счастливый.

int main() {
    /* Программа проверки счастливого билета. */
}

Шаги решения задачи:

int main(void) {
    /* TODO: Получить номер билета —
       неотрицательное число из 6 десятичных цифр. */

    /* TODO: Проверить, является ли билет счастливым. */

    /* TODO: Вывести результат проверки. */

    return 0;
}

Реализуем очевидные шаги:

#include <stdio.h>

int main(void) {
    long num;
    int lucky;

    /* Получить номер билета */
    scanf("%ld", &num);

    /* TODO: Проверить, является ли билет счастливым. */

    /* Вывести результат проверки. */
    if (lucky)
        printf("Да\n");
    else
        printf("Нет\n");

    return 0;
}

Шаг проверки можно реализовать в виде отдельной подпрограммы:

/* Проверяет, является ли num номером счастливого билета. */
int is_lucky(long num);

int main(void) {
    long num;
    int lucky;

    /* Получить номер билета */
    scanf("%ld", &num);

    /* Проверить, является ли билет счастливым. */
    lucky = is_lucky(num);

    /* Вывести результат проверки. */
    if (lucky)
        printf("Да\n");
    else
        printf("Нет\n");

    return 0;
}

int is_lucky(long num) {
    /* TODO: Разбить номер билета на 2 части по 3 цифры. */
    /* TODO: Вычислить суммы цифр в каждой части. */
    /* TODO: Билет счастливый, если суммы совпадают. */
}

Частичная реализация:

int is_lucky(long num) {
    long left, right;
    int left_sum, right_sum;

    /* Разбить номер билета на 2 части по 3 цифры. */
    left = num / 1000;
    right = num % 1000;

    /* TODO: Вычислить суммы цифр в каждой части. */

    /* Билет счастливый, если суммы совпадают. */
    return left_sum == right_sum;
}

Вычисление суммы выносим в отдельную функцию:

/* Возвращает сумму десятичных цифр num. */
int sum_digits(long num);

int is_lucky(long num) {
    long left, right;
    int left_sum, right_sum;

    /* Разбить номер билета на 2 части по 3 цифры. */
    left = num / 1000;
    right = num % 1000;

    /* Вычислить суммы цифр в каждой части. */
    left_sum = sum_digits(left);
    right_sum = sum_digits(right);

    /* Билет счастливый, если суммы совпадают. */
    return left_sum == right_sum;
}

int sum_digits(long num) {
    int sum = 0;

    while (num > 0) {
        sum += num % 10;
        num /= 10;
    }

    return sum;
}

Каждая функция решает свою задачу, проста для понимания и может быть отдельно протестирована. Кроме этого, вынесение алгоритм суммирования цифр в функцию sum_digits позволило избежать повторения кода.

Программа полностью:

#include <stdio.h>

/* Проверяет, является ли num номером счастливого билета. */
int is_lucky(long num);

/* Возвращает сумму десятичных цифр num. */
int sum_digits(long num);

int main(void) {
    long num;
    int lucky;

    /* Получить номер билета */
    scanf("%ld", &num);

    /* Проверить, является ли билет счастливым. */
    lucky = is_lucky(num);

    /* Вывести результат проверки. */
    if (lucky)
        printf("Да\n");
    else
        printf("Нет\n");

    return 0;
}

int is_lucky(long num) {
    long left, right;
    int left_sum, right_sum;

    /* Разбить номер билета на 2 части по 3 цифры. */
    left = num / 1000;
    right = num % 1000;

    /* Вычислить суммы цифр в каждой части. */
    left_sum = sum_digits(left);
    right_sum = sum_digits(right);

    /* Билет счастливый, если суммы совпадают. */
    return left_sum == right_sum;
}

int sum_digits(long num) {
    int sum = 0;

    while (num > 0) {
        sum += num % 10;
        num /= 10;
    }

    return sum;
}

Теперь вынесем код проверки счастливого билета в отдельный модуль:

lucky.c:

/* Проверяет, является ли num номером счастливого билета. */
int is_lucky(long num);

/* Возвращает сумму десятичных цифр num. */
int sum_digits(long num);

int is_lucky(long num) {
    long left, right;
    int left_sum, right_sum;

    /* Разбить номер билета на 2 части по 3 цифры. */
    left = num / 1000;
    right = num % 1000;

    /* Вычислить суммы цифр в каждой части. */
    left_sum = sum_digits(left);
    right_sum = sum_digits(right);

    /* Билет счастливый, если суммы совпадают. */
    return left_sum == right_sum;
}

int sum_digits(long num) {
    int sum = 0;

    while (num > 0) {
        sum += num % 10;
        num /= 10;
    }

    return sum;
}

main.c:

#include <stdio.h>

/* Проверяет, является ли num номером счастливого билета. */
int is_lucky(long num);

int main(void) {
    long num;
    int lucky;

    /* Получить номер билета */
    scanf("%ld", &num);

    /* Проверить, является ли билет счастливым. */
    lucky = is_lucky(num);

    /* Вывести результат проверки. */
    if (lucky)
        printf("Да\n");
    else
        printf("Нет\n");

    return 0;
}

Сборка:

gcc -c main.c
gcc -c lucky.c
gcc -o luckycheck main.o lucky.o

Проблема: прототип is_lucky нужно копировать в модуль main.c. А если модуль предоставляет множество полезных функций, то придется копировать все прототипы.

Решение: выносим прототип в отдельный файл lucky.h:

/* Проверяет, является ли num номером счастливого билета. */
int is_lucky(long num);

И включаем его в lucky.c и main.c:

#include "lucky.h"

Имя заголовочного файла в двойных кавычках — поиск выполняется относительно каталога исходного файла. В угловых скобках (<>) — поиск в системных каталогах.

Определим свой тип для результата проверки в lucky.h:

typedef enum {
    UNLUCKY,
    LUCKY
} lucky_status;

/* Возвращает LUCKY, если билет num счастливый, UNLUCKY — иначе. */
lucky_status is_lucky(long num);

Теперь тип lucky_status может использоваться в любом модуле, который включает lucky.h. Можно добавить еще один статус: номер билета не в диапазоне от 0 до 999999 и проверять еще и это.

Также добавим защиту от повторного включения:

#ifndef LUCKY_H
#define LUCKY_H

typedef enum {
    UNLUCKY,
    LUCKY
} lucky_status;

lucky_status is_lucky(long num);

#endif /* LUCKY_H */

Итоговый main.c:

#include <stdio.h>
#include "lucky.h"

int main(void) {
    long num;
    lucky_status lucky;

    /* Получить номер билета */
    scanf("%ld", &num);

    /* Проверить, является ли билет счастливым. */
    lucky = is_lucky(num);

    /* Вывести результат проверки. */
    if (lucky == LUCKY)
        printf("Да\n");
    else
        printf("Нет\n");

    return 0;
}

Итоговый lucky.c. К sum_digits добавили ключевое слово static, чтобы функция не была видима из других модулей:

#include "lucky.h"

/* Возвращает сумму десятичных цифр num. */
static int sum_digits(long num);

/* Возвращает LUCKY, если билет num счастливый, UNLUCKY — иначе. */
lucky_status is_lucky(long num) {
    long left, right;
    int left_sum, right_sum;

    /* Разбить номер билета на 2 части по 3 цифры. */
    left = num / 1000;
    right = num % 1000;

    /* Вычислить суммы цифр в каждой части. */
    left_sum = sum_digits(left);
    right_sum = sum_digits(right);

    /* Билет счастливый, если суммы совпадают. */
    return (left_sum == right_sum) ? LUCKY : UNLUCKY;
}

static int sum_digits(long num) {
    int sum = 0;

    while (num > 0) {
        sum += num % 10;
        num /= 10;
    }

    return sum;
}

И вариант Makefile для сборки всей программы:

luckycheck: main.o lucky.o
        gcc -o luckycheck main.o lucky.o

main.o: main.c lucky.h
        gcc -c main.c

lucky.o: lucky.c lucky.h
        gcc -c lucky.c

Подключение библиотек

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

#define RAD(a) (a * (3.14 / 180))

int main(void) {
    int i;
    for (i = 0; i < 360; i++) {
        printf ("%4d %6.4lf\n", i, cos(RAD(i)));
    }
    return EXIT_SUCCESS;
}
$ gcc -o mathsample mathsample.c
/usr/bin/ld: /tmp/cceJg4LG.o: в функции «main»:
mathsample.c:(.text+0x31): неопределённая ссылка на «cos»
collect2: ошибка: выполнение ld завершилось с кодом возврата 1

$ gcc -lm -o mathsample mathsample.c
$

Символ $ здесь и далее обозначает приглашение интерпретатора командной строки.

Создание разделяемой динамической библиотеки

Вместо встраивания модулей в исполняемый файл, можно загружать их динамически при запуске программы. Такие модули компонуются в разделяемые динамические библиотеки, которые могут размещаться в системе один раз и использоваться многими программами.

$ gcc -shared -o liblucky.so -fPIC lucky.c
$ gcc -L. -llucky -o luckycheck main.c
  • -fPIC включает генерацию кода, не зависящего от размещения в адресном пространстве процесса (position-independent code),
  • -L. задает путь для поиска библиотек: . — текущий каталог,
  • -llucky — использовать библиотеку liblucky.so.

SO — Shared object (разделяемый объект). Аналогичный механизм в Windows — файлы DLL (Dynamic-link library).

$ ./luckycheck
luckycheck: error while loading shared libraries: liblucky.so: cannot open shared object file: No such file or directory

Не находит, т. к. библиотека не размещена в каталогах поиска динамического редактора связей. Но можно добавить нужный каталог, используя переменную окружения LD_LIBRARY_PATH:

$ LD_LIBRARY_PATH=. ./luckycheck

Можно посмотреть, какие библиотеки использует исполняемый файл:

$ ldd /bin/ls
        linux-vdso.so.1 (0x00007ffef29ee000)
        libselinux.so.1 => /lib64/libselinux.so.1 (0x00007f354be88000)
        libcap.so.2 => /lib64/libcap.so.2 (0x00007f354be7e000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f354bc00000)
        libpcre2-8.so.0 => /lib64/libpcre2-8.so.0 (0x00007f354bb63000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f354bef9000)
$ ldd luckycheck
        linux-vdso.so.1 (0x00007ffcb7be8000)
        liblucky.so => not found
        libc.so.6 => /lib64/libc.so.6 (0x00007f8553000000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f8553397000)
$ LD_LIBRARY_PATH=. ldd luckycheck
        linux-vdso.so.1 (0x00007ffe141a4000)
        liblucky.so => ./liblucky.so (0x00007f80ae993000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f80ae600000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f80ae99a000)

Функция main и аргументы командной строки

Варианты определения функции main():

int main (void) {…}
int main (int argc, char *argv[]) {…}
int main (int argc, char *argv[], char *envp[]) {…}
  • argc — количество аргументов командной строки.
  • argv — аргументы — массив строк (указателей char *).
  • envp — массив переменных окружения. Количество переменных не указано, вместо этого конец массива обозначается значением NULL. Вместо envp удобно использовать функцию getenv() из stdlib.h.

Вместо char *argv[] и char *envp[] можно использовать эквивалентные определения char **argv и char **envp.

Нулевой аргумент argv[0] содержит имя программы, как оно было задано в команде.

Пример:

#include <stdio.h>
#include <stdlib.h>

int main (int argc, char *argv[]) {
    /* Прежде, чем использовать аргументы, проверяем их количество. */
    if (argc != 2) {
        fprintf(stderr, "A single argument is required.\n");
        /* Обычно argv[0] определен, но это не гарантировано. */
        if (argc > 0)
            /* Подсказка о правильном использовании программы. */
            fprintf(stderr, "Usage: %s name\n", argv[0]);
        return EXIT_FAILURE;
    }

    fprintf(stdout, "Hello, %s!\n", argv[1]);
    return EXIT_SUCCESS;
}

При наличии лишних аргументов тоже следует сообщать об ошибке: вероятно пользователь делает что-то неправильно и не получит ожидаемого результата.

$ ./hello 
A single argument is required.
Usage: ./hello name
$ ./hello student
Hello, student!
$ ./hello another student
A single argument is required.
Usage: ./hello name
$ ./hello "another student"
Hello, another student!

Диагностические сообщения (ошибки, предупреждения, отладочные сообщения) следует выводить в stderr, чтобы их можно было отделить от основного вывода программы:

$ ./hello > output.txt
A single argument is required.
Usage: ./hello name
$ ./hello student > output.txt

Символ > перенаправляет stdout в указанный файл, но stderr остается связан с терминалом. Таким образом, сообщение об ошибке отобразилось в терминале, но сообщение "Hello, student!" было записано в файл output.txt.

Потоковый ввод-вывод

Поток — набор внутренних структур данных, обеспечивающих последовательный интерфейс обмена между внешним источником (приемником) и программой.

Потоковый ввод-вывод

  • обеспечивает последовательную передачу байт,
  • скрывает детали реализации I/O на уровне ОС.

Функции работы с потоками позволяют:

  • открывать и закрывать потоки;
  • осуществлять ввод-вывод символов, строк, форматированных данных, блоков заданной длины;
  • перемещать указатель текущей позиции в потоке;
  • управлять буферизацией;
  • анализировать состояния, диагностировать ошибки.

Для каждого потока создается идентифицирующий его указатель FILE *.

Открытие потока для чтения из файла:

FILE *f;

f = fopen("path/to/file", "r");
if (f == NULL) {
    fprintf(stderr, "Error opening file!\n");
    exit(EXIT_FAILTURE);
}

В случае ошибки fopen возвращает NULL и присваивает код ошибки в переменную errno. Для всех функций, определяющих код в errno, сообщения об ошибке можно выводить с помощью perror() — так пользователь получит более точную информацию о проблеме:

FILE *f;

f = fopen("path/to/file", "r");
if (f == NULL) {
    perror("Error opening file");
    exit(EXIT_FAILTURE);
}

Пример сообщения, которое может быть выведено при таком вызове perror():

$ ./main
Error opening file: No such file or directory

Режимы открытия потока (второй аргумент fopen()):

  • "r" — открыть для чтения;
  • "r+" — открыть для чтения и записи;
  • w — обрезать файл до нулевой длины или создать файл для записи;
  • "w+" — открыть для чтения и записи, создать файл, если он не существует;
  • "a" — открыть для добавления (записи в конец файла);
  • "a+" — открыть для чтения и добавления.

В строку режима можно добавить символ b для включения двоичного (а не текстового) режима работы с файлами. В большинстве систем это не имеет значения.

fclose(f) — закрытие потока. Может вернуть ошибку (ненулевое значение)! Иногда ошибка записи может обнаружиться только при закрытии потока. Важно, чтобы пользователь своевременно узнал о том, что данные не были записаны, поэтому проверка необходима.

Посимвольный ввод-вывод

int fgetc(FILE *stream);
int getc(FILE *stream);
int getchar(void);

int fputc(int c, FILE *stream);
int putc(int c, FILE *stream);
int putchar(int c);
int ungetc(int c, FILE *stream);

Функции fgetc(), getc() и getchar() возвращают символ, считанный как unsigned char и преобразованный в int, или EOF по достижении конца файла или при возникновении ошибки. При успешном выполнении функция ungetc() возвращает c или EOF при ошибке.

Функции fputc(), putc() и putchar() возвращают записанный символ, преобразованный из unsigned char в int или EOF в случае ошибки.

Прочитать весь файл посимвольно:

while ((c = fgetc(f)) != EOF) {
    …
}

Цикл чтения завершился из-за ошибки или в результате достижения конца файла?

int feof(FILE *stream);
int ferror(FILE *stream);
  • feof(f) — истина (не 0), если достигнут конец файла.
  • ferror(f) — истина, если была ошибка.

Построчный ввод-вывод

char *fgets(char *s, int size, FILE *stream);
char *gets(char *s); /* Никогда не используйте эту функцию! */

int fputs(const char *s, FILE *stream);
int puts(const char *s);

Считывание строки и удаление '\n' в конце:

#define LINE_MAX 100

char s[LINE_MAX];

if (!fgets(s, LINE_MAX, stdin)) {
    /* TODO: обработать ошибку. */
}

/* Удаление возможного '\n' в конце строки. */
size_t n = strlen(s);
if (s[n - 1] == '\n')
    s[n - 1] = '\0';

Блочный ввод-вывод

size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream);

При успешном выполнении функции fread() и fwrite() возвращают количество считанных или записанных единиц. Это количество равно количеству переданных байт только, если значение size равно 1. В случае ошибки или по достижении конца файла возвращается меньшее количество единиц (или ноль).

Чтение блоками до конца файла:

FILE *in = …;
char buf[BUFSIZ];
size_t n;

do {
    n = fread(buf, 1, BUFSIZ, in);
    printf("Прочитано байт: %zu\n", n);
} while (n == BUFSIZ);

if (ferror(in)) {
    perror("Ошибка чтения");
    exit(EXIT_FAILURE);
}

Константа BUFSIZ определена в stdio.h и задает рекомендуемый размер буфера ввода-вывода.

char buf[10]
fread(buf, 1, sizeof(buf), stdin);
printf("Прочитано: %s\n", buf); /* !!! */

Почему так нельзя?

Позиционирование в потоке

int fseek(FILE *stream, long offset, int whence);
long ftell(FILE *stream);
void rewind(FILE *stream);
int fgetpos(FILE *stream, fpos_t *pos);
int fsetpos(FILE *stream, const fpos_t *pos);

Значения whence:

  • SEEK_SET — смещение относительно начала,
  • SEEK_CUR — относительно текущей позиции,
  • SEEK_END — относительно конца файла.

Не все потоки допускают произвольное изменение позиции. В случае ошибки функции fseek(), ftell(), fgetpos() и fsetpos() возвращают -1 и устанавливают значение errno.

Форматный вывод

int printf(const char *format, ...);
int fprintf(FILE *stream, const char *format, ...);
int sprintf(char *str, const char *format, ...);
int snprintf(char *str, size_t size, const char *format, ...);

При успешном выполнении возвращают количество напечатанных символов. Если возникла ошибка, то возвращается отрицательное значение.

Форматный ввод

int scanf(const char *format, ...);
int fscanf(FILE *stream, const char *format, ...);
int sscanf(const char *str, const char *format, ...);

При успешном выполнении данные функции возвращают количество входных совпавших и назначенных элементов, которое может быть меньше, чем предусматривалось, или даже равно нулю, в случае преждевременной ошибки при поиске совпадений. Если конец входных данных был достигнут раньше, чем произошло хотя бы одно совпадение или при ошибке совпадения возвращается значение EOF. Значение EOF также возвращается при ошибке чтения.

Обработка строк

Строка в Си представляется как массив символов, заканчивающийся символом '\0' (терминальный ноль).

/* Массим из 6 символов */
char str1[] = "Hello";

/* Эквивалентно */
char str2[] = {'H', 'e', 'l', 'l', 'o', '\0'};

/* Указатель на неизменяемую строку */
char *str3 = "Hello";

Массив строк

  • как двумерный массив

    char seasons[][7] = {
        "Winter",
        "Spring",
        "Summer",
        "Fall"
    };
    
    for (int i = 0; i < 4; i++)
        printf("%s\n", seasons[i]);
    

    string_array.png

  • как массив указателей

    char *seasons[] = {
        "Winter",
        "Spring",
        "Summer",
        "Fall"
    };
    
    for (int i = 0; i < 4; i++)
        printf("%s\n", seasons[i]);
    

    string_ptr_array.png

В первом примере строки изменять можно, во втором — нельзя:

char seasons[][7] = {
    "Winter",
    "Spring",
    "Summer",
    "Fall"
};

for (int i = 0; i < 4; i++) {
    seasons[i][0] = 'X';
    printf("%s\n", seasons[i]);
}
$ ./main
Xinter
Xpring
Xummer
Xall
char *seasons[] = {
    "Winter",
    "Spring",
    "Summer",
    "Fall"
};

for (int i = 0; i < 4; i++) {
    seasons[i][0] = 'X';
    printf("%s\n", seasons[i]);
}
$ ./main
ошибка сегментирования (core dumped)

Но можно создать массив указателей на изменяемые строки, используя динамическое выделение памяти:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main (void) {
    char *seasons[4];

    /* Выделение памяти под каждую строку */
    for (int i = 0; i < 4; i++) {
        seasons[i] = (char *) malloc(7);
        if (seasons[i] == NULL) {
            perror("Can't allocate memory");
            return EXIT_FAILURE;
        }
    }

    /* Копирование строк в выделенную память */
    strcpy(seasons[0], "Winter");
    strcpy(seasons[1], "Spring");
    strcpy(seasons[2], "Summer");
    strcpy(seasons[3], "Fall");

    /* Изменение первого символа и вывод */
    for (int i = 0; i < 4; i++) {
        seasons[i][0] = 'X';
        printf("%s\n", seasons[i]);
    }

    return EXIT_SUCCESS;
}

Передача двумерного массива в функцию:

#include <stdio.h>

#define SEASON_MAX 7

void print_seasons(char seasons[][SEASON_MAX]) {
    /* или char (*seasons)[SEASON_MAX] —
       указатель на char[SEASON_MAX] */
    for (int i = 0; i < 4; i++) {
        printf("%s\n", seasons[i]);
    }
}

int main(void) {
    char seasons[][SEASON_MAX] = {
        "Winter",
        "Spring",
        "Summer",
        "Fall"
    };

    print_seasons(seasons);
    return 0;
}

Передача массива указателей в функцию:

#include <stdio.h>

void print_seasons(char *seasons[]) {
    /* или char **seasons — указатель на указатели на char */
    for (int i = 0; i < 4; i++) {
        printf("%s\n", seasons[i]);
    }
}

int main(void) {
    char *seasons[] = {
        "Winter",
        "Spring",
        "Summer",
        "Fall"
    };

    print_seasons(seasons);
    return 0;
}

Таким образом, массив строк может быть представлен как двумерный массив и как массив указателей, и это не одно и то же.

Аргументы командной строки: argv — это массив указателей, причем последнее значение в нем NULL.

$ ./pr -x 1.txt

string_argv.png

#include <stdio.h>

void print_args(char *argv[]) {
    /* Используем особенность, что массив argv
       заканчивается значением NULL */
    for (int i = 0; argv[i]; i++) {
        printf("%s\n", argv[i]);
    }
}

int main(__attribute__((unused)) int argc, char *argv[]) {
    /* Для примера выше напечатает:
       ./pr
       -x
       1.txt
    */
    print_args(argv);

    /* Для примера выше напечатает без argv[0]:
       -x
       1.txt
    */    
    print_args(argv + 1);

    return 0;
}

Выделение подстрок

Выводим различные части строки "substring", используя установку терминального нуля и арифметику указателей:

#include <stdio.h>
#include <string.h>

int main (void) {
    char s[] = "substring";
    int n = strlen(s);

    /* Последние 6 символов: "string" */
    printf("%s\n", s + n - 6);

    /* Первые 3 симола: "sub" */
    char c = s[3];
    s[3] = '\0';
    printf("%s\n", s);

    /* Восстанавливаем строку в исходное состояние */
    s[3] = c;

    /* Символы с 4 по 6: "str" */
    s[6] = '\0';
    printf("%s\n", s + 3);

    return 0;
}

Разбиение строки:

char s[] = "one two three";

printf("%s\n", s); /* "one two three" */

/* Записываем терминальные нули на позиции пробелов */
s[3] = '\0';
s[7] = '\0';

/* Получилось 3 строки: s, (s + 4) и (s + 8) */
printf("%s\n", s);     /* "one" */
printf("%s\n", s + 4); /* "two" */
printf("%s\n", s + 8); /* "three" */

Функция strsep() (нестандартная, но доступна в glibc) делает это автоматически:

char s[] = "one two three";
char *sp = s;

while (sp != NULL) {
    printf("%s\n", strsep(&sp, " "));
}

Функции для работы со строками

Длина строки (варианты реализации strlen() были разобраны ранее):

size_t strlen(const char *s) {
    const char *p = s;

    while (*p != '\0')
        p++;

    return p - s;
}

Конкатенация (соединение) строк. Функция добавляет строку s2 в конец строки s1:

char *strcat(char *s1, const char *s2)
{
    char *p = s1;

    /* Ищем терминальный ноль в первой строке */
    while (*p != '\0')
        p++;

    /* Копируем символы второй строки в первую, начиная с этой позиции
       и до конца второй строки */
    while (*s2 != '\0') {
        *p = *s2;
        p++;
        s2++;
    }

    /* Далее устанавливаем терминальный ноль */
    *p = '\0';

    return s1;
}

Более компактная реализация:

char *strcat(char *s1, const char *s2)
{
    char *p = s1;

    while (*p)
        p++;

    /* Цикл закончится, когда будет присвоен '\0' */
    while ((*p++ = *s2++));

    return s1;
}

Двойные скобки в while использованы только для того, чтобы показать, что это намеренное присваивание, а не забытый символ в операторе сравнения. Без них GCC с -Wall выдаст предупреждение.

В стандартной библиотеке string.h определены функции для обработки строк:

/* Копирует строку из src в dest, возвращая указатель на конец строки
   результата в dest. */
char *stpcpy(char *dest, const char *src);

/* Добавляет строку src к строке dest, возвращая указатель на dest. */
char *strcat(char *dest, const char *src);

/* Возвращает указатель на местонахождение первого совпадения с
   символом c в строке s. */
char *strchr(const char *s, int c);

/* Сравнивает строки s1 и s2. */
int strcmp(const char *s1, const char *s2);

/* Сравнивает строки s1 и s2, применяя правила текущей локали. */
int strcoll(const char *s1, const char *s2);

/* Копирует строку src в dest, возвращая указатель на начало строки
   в dest. */
char *strcpy(char *dest, const char *src);

/* Вычисляет длину начального сегмента строки s, состоящего только
   из байт, не указанных в строке reject. */
size_t strcspn(const char *s, const char *reject);

/* Возвращает копию строки s, память для которой выделяется с
   помощью malloc(3). */
char *strdup(const char *s);

/* Переставляет символы в string в произвольном порядке. */
char *strfry(char *string);

/* Возвращает длину строки s. */
size_t strlen(const char *s);

/* Добавляет не более n байт из строки src в строку dest, возвращая
   указатель на dest. */
char *strncat(char *dest, const char *src, size_t n);

/* Сравнивает не более n байт строк s1 и s2. */
int strncmp(const char *s1, const char *s2, size_t n);

/* Копирует не более n байт из строки src в строку dest, возвращая
   указатель на dest. */
char *strncpy(char *dest, const char *src, size_t n);

/* Возвращает первое появление в строке s любых байтов из строки
   accept. */
char *strpbrk(const char *s, const char *accept);

/* Возвращает указатель на местонахождение последнего совпадения с
   символом c в строке s. */
char *strrchr(const char *s, int c);

/* Извлекает начальный токен из stringp, который отделён одним из
   байтов из delim. */
char *strsep(char **stringp, const char *delim);

/* Вычисляет длину начального сегмента из строки s, состоящего
   только из байт, указанных в accept. */
size_t strspn(const char *s, const char *accept);

/* Ищет первое соответствие подстроки needle в строке haystack и
   возвращает указатель на найденную подстроку. */
char *strstr(const char *haystack, const char *needle);

/* Извлекает токены из строки s, которые отделены одним из байтов
   из delim. */
char *strtok(char *s, const char *delim);

/* Преобразует src в текущую локаль и копирует первые n байт в
   dest. */
size_t strxfrm(char *dest, const char *src, size_t n);

А также функции для работы с последовательностями байтов, которые не обязательно должны быть строками (могут не заканчиваться нулевым символом и содержать нулевые символы в любом месте):

/* Копирование */
void *memcpy(void *dest, const void *src, size_t n);
void *memccpy(void *dest, const void *src, int c, size_t n);
void *memmove(void *dest, const void *src, size_t n);

/* Поиск */
void *memchr(const void *s, int c, size_t n);
void *memrchr(const void *s, int c, size_t n);
void *rawmemchr(const void *s, int c);

/* Сравнение */
int memcmp(const void *s1, const void *s2, size_t n);

/* Заполнение */
void *memset(void *s, int c, size_t n);

Пример использования strcmp() для проверки совпадения подстроки. Функция возвращает истину, если конец строки s совпадает со строкой suffix:

int ends_with(const char *s, const char *suffix) {
    size_t slen = strlen(s);
    size_t suffixlen = strlen(suffix);

    if (slen < suffixlen)
        return 0;

    return strcmp(s + slen - suffixlen, suffix) == 0;
}

Пример использования strncmp() для проверки совпадения подстроки. Функция возвращает истину, если начало строки s совпадает со строкой prefix:

int starts_with(const char *s, const char *prefix) {
    size_t slen = strlen(s);
    size_t prefixlen = strlen(prefix);

    if (slen < prefixlen)
        return 0;

    return strncmp(s, prefix, prefixlen) == 0;
}

Работа с символами

В ctype.h определены функции для определения категорий и преобразования отдельных однобайтовых символов:

int   isalnum(int);
int   isalpha(int);
int   isascii(int);
int   isblank(int);
int   iscntrl(int);
int   isdigit(int);
int   isgraph(int);
int   islower(int);
int   isprint(int);
int   ispunct(int);
int   isspace(int);
int   isupper(int);
int   isxdigit(int);
int   toascii(int);
int   tolower(int);
int   toupper(int);

Например, isspace() проверяет, являются ли символы не отображаемыми. В локали «C» таковыми являются: пробел, символ перевода страницы '\f', символ новой строки '\n', символ перевода каретки '\r', символ горизонтальной табуляции '\t' и символ вертикальной табуляции '\v'.

Работа функций зависит от текущей локали. Доступны аналогичные функции с суффиксом _l и дополнительным аргументом типа locale_t для использования определенной локали.

Для работы с широкими символами (которые требуют более 8 бит для представления их кода), см. wchar.h и wctype.h. Вопрос обработки таких символов будет разобран позднее.

Структурный тип данных

Можно объединить несколько значений различных типов в один объект данных, например, для представления моделируемого объекта реального мира в виде набора его атрибутов.

struct point {
    double x;
    double y;
};

struct point location;

location.x = 1.5;
location.y = -2.0;

Структура — производный тип, определяемый совокупностью составляющих ее компонентов — полей. Каждый объект структурного типа содержит полный набор компонентов определения структуры.

struct person {
    char name[MAXLINE];
    char surname[MAXLINE];
    int age;
};

struct person student;

strcpy(student.surname, "Smith");
strcpy(student.name, "John");
student.age = 17;

Массивы при таком определении являются частью структуры: значение sizeof(struct person) будет включать их размеры.

Другой вариант:

struct person {
    char *name;
    char *surname;
    int age;
};

struct person student;

student.surname = "Smith";
student.name = "John";
student.age = 17;

В таком случае структура содержит только указатели на строки размещенные где-то еше в памяти, но не сами эти строки.

Можно определить массив структур:

int size_22101 = 23;
struct person st_22101[size_22101];

/* ... */

for (i = 0; i < size_22101; i++) {
    printf("%s %s\t%d\n",
           st_22101[i].name,
           st_22101[i].surname,
           st_22101[i].age);
}

Объект структурного типа может содержать другие структуры в качестве своих компонентов:

struct point {
    double x;
    double y;
};

struct line {
    struct point p1;
    struct point p2;
};

/* Инициализация — как для массива — перечисление значений
   в фигурных скобрах: */
struct line l = {{0.0, -1.0}, {1.0, 2.0}};

/* sqrt доступна в math.h */
double len = sqrt((l.p1.x - l.p2.x) * (l.p1.x - l.p2.x) +
                  (l.p1.y - l.p2.y) * (l.p1.y - l.p2.y));

С использованием typedef структурам могут быть присвоены короткие имена типов:

typedef struct _point {
    double x;
    double y;
} point;

typedef struct _line {
    point p1;
    point p2;
} line;

line l = {{0.0, -1.0}, {1.0, 2.0}};

Указатель на структуру:

struct point {
    double x;
    double y;
};

struct point p = {1.0, 2.0};
struct point *pptr = &p;

/* 2 способа обратиться к полю структуры по указателю: */
double px = (*pptr).x;
double py = pptr->y;

Битовые поля и объединения

Пример: представление даты в файловой системе FAT.

struct file_date {
    uint16_t day: 5;
    uint16_t month: 4;
    uint16_t year: 7;
};

struct file_date mdate;

mdate.day = 11;
mdate.month = 11;
mdate.year = 42; /* 1980 + 42 = 2022 */

Объединения (union): память одновременно используется несколькими полями, можно интерпретировать те же байты как разные типы данных:

union int_date {
    uint16_t num;
    struct file_date fd;
};

union int_date u;
u.num = 12345;
printf("%d %d %d\n", u.fd.day, u.fd.month, u.fd.year + 1980);

Присваивание и передача в функцию

#include <stdio.h>
#include <string.h>

#define MAXLINE 1024

struct person {
    char name[MAXLINE];
    char surname[MAXLINE];
    int age;
};

void print_person(struct person p) {
    printf("%s %s, %d\n", p.name, p.surname, p.age);

    /* Не изменит значения student.name и student.age в main */
    strcpy(p.name, "Jane");
    p.age = 18;
}

void print_person_ptr(struct person *p) {
    printf("%s %s, %d\n", p->name, p->surname, p->age);

    /* Изменит значение в структуре, переданной по указателю */
    strcpy(p->name, "Jane");
    p->age = 18;
}

int main(void) {
    struct person student;

    strcpy(student.surname, "Smith");
    strcpy(student.name, "John");
    student.age = 17;

    struct person student_copy;

    /* Вся структура копируется, включая массивы */
    student_copy = student;

    /* В функцию также передается копия структуры
       (передача по значению) */
    print_person(student);      /* John Smith, 17 */
    print_person(student);      /* John Smith, 17 */
    print_person_ptr(&student); /* John Smith, 17 */
    print_person(student);      /* Jane Smith, 18 */
    print_person(student_copy); /* John Smith, 17 */

    return 0;
}

Абстракция данных

Абстракции в программировании, язык Си, уровни абстракции, типы данных.

Пример (файловый ввод-вывод)
Стандартная библиотека ввода-вывода (stdio.h)
Поток FILE *
API POSIX
Системные вызовы
Подсистема ввода-вывода ОС
Файловая система
Драйвер устройства
Устройство хранения

Кроме этого, потоки позволяют работать не только с файлами на диске. Что представляет FILE * в Си?

Абстракция данных связывает тип данных с набором операций над ним. Пользователь типа данных не имеет прямого доступа к его реализации, но может работать с данными через предоставленный набор операций.

Абстрактный тип данных (АТД) — математическая модель типа данных. АТД определяется операциями, которые применимы к объектам типа, и их свойствами.

Например, АТД для пары (базовый тип данных во многих диалектах языка Лисп) поддерживает операции: car, cdr и конструктор cons, такие что для произвольных a и b:

  • \(car(cons(a, b)) = a\)
  • \(cdr(cons(a, b)) = b\)

АТД стека можно определить через операции push(s, a), pop(s) и empty(s), применяемые к стеку s и произвольному значению a. Эти операции должны обладать следующими свойствами:

  • стек изначально является пустым;
  • empty(s) истинно тогда и только тогда, когда стек пустой.
  • операция push добавляет элемент к стеку,
  • операция pop применима только к непустому стеку, удаляет последний добавленный элемент и возвращает его значение, стек возвращается к состоянию до предшествующего push.

Это императивное определение стека: объект АТД обладает внутренним состоянием, которое может изменятся в результате выполнения операций.

Аппликативное определение: представляем операции как функции, возвращающие новое состояние. Операции для АТД стэка \(\mathrm{STACK[T]}\), содержащего элементы типа \(\mathrm{T}\):

  • \(new: \mathrm{STACK[T]}\)
  • \(empty: \mathrm{STACK[T]} \to {0, 1}\)
  • \(push: \mathrm{STACK[T] \times T} \to \mathrm{STACK[T]}\)
  • \(pop: \{s \in \mathrm{STACK[T]}\ |\ empty(s) = 0\} → \mathrm{STACK[T]} \times \mathrm{T}\)
    (pop применима только к непустому стеку)

Аксиомы:

  • \(empty(new) = 1\)
  • \(empty(push(s, x)) = 0, \forall s \in \mathrm{STACK[T]}, x \in \mathrm{T}\)
  • \(pop(push(s, x)) = (s, x), \forall s \in \mathrm{STACK[T]}, x \in \mathrm{T}\)

Получилось формальное математическое определение стека. Можно строить выражения из этих функций и доказывать какие-то свойства стека.

АТД предполагает возможность различных реализаций, удовлетворяющих его определению.

Связное представление и структуры данных

В определении структурного типа может присутствовать указатель на этот же структурный тип.

Связный список

Связный список — базовая динамическая структура данных, состоящая из узлов, каждый из которых содержит собственно данные и одну или две ссылки («связи») на следующий и/или предыдущий узел списка.

  • Односвязный:

    struct node {
        T value;
        struct node *next;
    };
    
  • Двухсвязный:

    struct node {
        T value;
        struct node *prev;
        struct node *next;
    };
    

Связный список обеспечивает сложность вставки и удаления элемента \(O(1)\), однако сложность доступа к элементу по его индексу составляет \(O(n)\).

На основе связных списков могут быть реализованы различные абстрактные типы данных коллекций:

  • Список
  • Стек
  • Очередь
  • Дек (двухсторонняя очередь)
  • Кольцевой список

Пример реализации некоторых АТД с использованием связного списка.

Двоичное дерево

Иерархическая структура данных, в которой каждый узел имеет не более двух потомков.

struct node {
    T value;
    struct node *left;
    struct node *right;
    struct node *parent; /* необязательно */
};

Двоичное дерево поиска — двоичное дерево, для которого выполняются следующие дополнительные условия:

  • оба поддерева (левое и правое) являются двоичными деревьями поиска;
  • у всех узлов левого поддерева произвольного узла X значения ключей данных меньше значения ключа данных самого узла X;
  • у всех узлов правого поддерева произвольного узла X значения ключей данных больше либо равны значению ключа данных самого узла X.

Можно использовать для реализации ассоциативного массива, сопоставляющего ключи значениям. В отличие от обычного массива, ключи ассоциативного массива не обязательно являются последовательными числовыми индексами 0, 1, 2, …

Ассоциативный массив — абстрактный тип данных, позволяющий хранить пары вида (ключ, значение) и поддерживающий операции добавления пары, а также поиска и удаления пары по ключу:

  • insert(ключ, значение) — добавить или заменить пару c указанным ключом;
  • find(ключ) — найти значение по ключу;
  • remove(ключ) — удалить пару с указанным ключом.

Ассоциативный массив не может хранить две пары с одинаковыми ключами.

Для реализации можно использовать деревья поиска или хеш-таблицы.

Динамическое выделение памяти

#include <stdlib.h>

void *malloc(size_t size);
void free(void *ptr);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);
void *reallocarray(void *ptr, size_t nmemb, size_t size);

Вызов calloc(nmemb, size) аналогичен вызову malloc(nmemb * size), но при использовании calloc:

  • выделенная память обнуляется,
  • переполнение nmemb * size приводит к ошибке, а не к выделению неправильного количества памяти.

realloc(ptr, size) изменяет размер ранее выделенного блока памяти ptr, выполняя копирование и освобождение старого блока, если дополнить его не получается. Возвращаемый указатель может отличаться или не отличаться от ptr!

Утечки памяти

По мере работы программы выделяется все больше памяти, которая никогда не освобождается. Указатели на выделенную память заменяются другими значениями, что делает освобождение невозможным.

#include <stdio.h>
#include <stdlib.h>

typedef struct {
    float real;
    float img;
} complex;

complex *make_complex(float real, float img) {
    complex *c = malloc(sizeof(complex));

    if (c == NULL) {
        perror("malloc");
        exit(EXIT_FAILURE);
    }

    c->real = real;
    c->img = img;

    return c;
}

complex *cmpl_sum(const complex *a, const complex *b) {
    return make_complex(a->real + b->real,
                        a->img + b->img);
}

int main(void) {
    complex *sum = make_complex(0, 0);
    complex *c = make_complex(0, 0);

    while(scanf("%f%f", &c->real, &c->img) == 2) {
        /* Утечка памяти!!! */
        sum = cmpl_sum(sum, c);
        printf("Сумма: %f + %fi\n", sum->real, sum->img);
    }

    return EXIT_SUCCESS;
}

Возможное решение: использование автоматическог сборщика мусора (garbage collector), например, для Си доступен консервативный сборщик мусора Boehm–Demers–Weiser, предоставляющий макросы GC_MALLOC, GC_MALLOC_ATOMIC, и GC_REALLOC для замены стандартных функций.

Локализация и интернационализация

Заголовочный файл locale.h включает функции для управления теми средствами библиотеки, которые меняют поведение при смене локали — интегрального параметра, влияющего на различные региональные параметры. Текущую локаль можно задать с помощью функции setlocale:

#include <locale.h>
char *setlocale(int category, const char *locale);
Категория Назначение
LC_ALL Локаль целиком
LC_ADDRESS Форматирование адресов и элементов, относящихся к географии
LC_COLLATE Сортировка строк
LC_CTYPE Классы символов
LC_IDENTIFICATION Метаданные, описывающие локаль
LC_MEASUREMENT Настройки, относящиеся к единицам измерения (метрические или системы мер США)
LC_MESSAGES Локализированные сообщения на родном языке
LC_MONETARY Форматирование значений денежных единиц
LC_NAME Форматирование приветствий людей
LC_NUMERIC Форматирование не денежных числовых значений
LC_PAPER Настройки стандартных размеров бумаги
LC_TELEPHONE Форматы, используемые в телефонных службах
LC_TIME Форматирование значений дат и времени

Если locale — пустая строка, то любая часть локали, которую требуется изменить, будет задана исходя из переменных окружения.

Типовое использование — в начале main():

setlocale(LC_ALL, "");

Имя локали обычно имеет формат язык[_территория][таблица символов][@модификатор].
Примеры: ru_RU.UTF-8, en_IE@euro.

По умолчанию используется переносимая локаль "C" — она существует во всех поддерживаемых системах.

Пример влияния локали на формат чисел:

#include <stdio.h>
#include <locale.h>

void print_float(float f, char *locale) {
    char *saved_locale = setlocale(LC_ALL, NULL);

    if (setlocale(LC_ALL, locale) == NULL) {
        printf("Локаль %s недоступна\n", locale);
        return;
    }

    /* ' включает группировку по тысячам,
       I разрешает использовать альтернативные символы для цифр */
    printf("%s: %'I.2f\n", locale, f);
    setlocale(LC_ALL, saved_locale);
}

int main(void) {
    /* Английский, США */
    print_float(1234.56, "en_US.UTF-8");

    /* Русский */
    print_float(1234.56, "ru_RU.UTF-8");

    /* Персидский, Иран */
    print_float(1234.56, "fa_IR.UTF-8");

    return 0;
}

Вывод:

en_US.UTF-8: 1,234.56
ru_RU.UTF-8: 1 234,56
fa_IR.UTF-8: ۱٬۲۳۴٫۵۶

Даже цифры могут быть другими (не стоит делать лишних предположений).

Многобайтовые кодировки и широкие символы

8 бит на символ недостаточно — всего 256 различных значений. Стандарт Юникод версии 15.0 определяет 149 186 символов и поддерживает 161 письменность. Распространенная кодировка UTF-8 использует 1 байт для символов ASCII и 2 байта для символов кириллицы, а некоторые коды представляются 4 байтами.

#include <stdlib.h>
int mblen(const char *s, size_t n);

Многобайтовые строки можно преобразовать в строки широких символов типа wchar_t.

#include <stdlib.h>

int mbtowc(wchar_t *pwc, const char *s, size_t n);
size_t mbstowcs(wchar_t *dest, const char *src, size_t n);

Для определения широких литералов используется префикс L:

wchar_t wc = L'😀';
/* или так: wchar_t wc = L'\U0001F600'; */
wchar_t *ws = L"фывапролдж";

Функции ввода-вывода доступны в wchar.h:

int wprintf(const wchar_t *format, ...);
int fwprintf(FILE *stream, const wchar_t *format, ...);

int fwscanf(FILE *stream, const wchar_t *format, ...);
int wscanf(const wchar_t *format, ...);

wint_t fputwc(wchar_t wc, FILE *stream);
int fputws(const wchar_t *ws, FILE *stream);

wint_t fgetwc(FILE *stream);
wchar_t *fgetws(wchar_t *ws, int n, FILE *stream);
#include <stdio.h>
#include <stdlib.h>
#include <wchar.h>
#include <locale.h>

int main(void) {
    wchar_t wc = L'\U0001F600';
    wchar_t *ws = L"фывапролдж";

    /* Без setlocale правильно не выведет */
    setlocale(LC_ALL, "");
    /* или так, если мы хотим всегда выводить в UTF-8:
       setlocale(LC_CTYPE, "C.UTF-8"); */

    fwprintf(stdout, L"%lc %ls\n", wc, ws);

    return 0;
}

Пример: определение длины строки в многобайтовой кодировке и вывод символов через пробел (С  Р А З Р Я Д К О Й):

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <locale.h>

int main() {
    setlocale(LC_CTYPE, "C.UTF-8");

    /* Строка в UTF-8 */
    char *s = "ПРИВЕТ";

    size_t n = strlen(s);
    printf("%zu\n", n); /* 12 */

    /* Пытаемся вывести символы через пробел —
       получается ерунда */
    for (int i = 0; i < n; i++)
        /* Добавляем пробел перед всеми символами,
           кроме начального */
        printf("%s%c", i > 0 ? " ": "", s[i]);
    printf("\n");

    /* Количество многобайтовых символов */
    size_t wn = mbstowcs(NULL, s, 0);

    /* Возможна ошибка: не любая последовательность байтов
       соответствует широкому символу, и нужна подходящая локаль
       (попробуйте закомментировать setlocale выше). */
    if (wn == (size_t) -1) {
        perror("mbstowcs");
        exit(EXIT_FAILURE);
    }

    printf("%zu\n", wn); /* 6 — правильно */

    /* Преобразуем s в длинную строку */
    wchar_t ws[wn + 1]; /* Количество символов плюс терминальный ноль */
    mbstowcs(ws, s, wn);

    /* Выводим широкие символы через пробел —
       теперь дейтсвительно выводит "П Р И В Е Т" */
    for (int i = 0; i < wn; i++)
        printf("%s%lc", i > 0 ? " " : "", ws[i]);
    printf("\n");

    return EXIT_SUCCESS;
}

Вывод

12
▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒ ▒
6
П Р И В Е Т

Функции для проверки типа широких символов, аналогичные функциям в ctype.h:

#include <wctype.h>

int iswalnum(wint_t);
int iswalpha(wint_t);
wint_t towlower(wint_t);
wint_t towupper(wint_t);
…

Дата и время

#include <time.h>

Типы:

  • clock_t длительность времени в абстрактных отсчетах;
  • time_t представляет время в секундах с некоторого момента в прошлом.
    • Время Юникс: количество секунд c 0:00:00 UTC 1 января 1970 г.
    • Проблема 2038 года: 32-битовое знаковое целое 231-1 соответствует 03:14:07 UTC 19 января 2038 г.
struct tm {
    int tm_sec;   // Seconds [0, 60]
    int tm_min;   // Minutes [0, 59]
    int tm_hour;  // Hour [0, 23]
    int tm_mday;  // Day of month [1, 31]
    int tm_mon;   // Month of year [0, 11]
    int tm_year;  // Years since 1900
    int tm_wday;  // Day of week [0, 6] (Sunday = 0)
    int tm_yday;  // Day of year [0, 365]
    int tm_isdst; // Daylight Savings flag
}

Обратите внимание: в минуте может быть дополнительная 61-я секунда.

Вывод текущего времени:

#include <stdio.h>
#include <time.h>

int main(void) {
    struct tm *tm;
    time_t lt;

    lt = time(NULL);

    /* Местное время */
    tm = localtime(&lt);
    printf("%s", asctime(tm));

    /* UTC */
    tm = gmtime(&lt);
    printf("%s", asctime(tm));

    return 0;
}

Измерения времени выполнения кода:

#include <stdio.h>
#include <time.h>

int main() {
    clock_t t;
    t = clock();

    /* ... измеряемый код ... */

    t = clock() - t;
    printf("%ld ticks (%f seconds).\n", t, ((float)t) / CLOCKS_PER_SEC);

    return 0;
}

Для измерения времени с потенциальной точностью до наносекунд (API POSIX, не входит в стандартный C):

struct timespec {
    time_t tv_sec; // Seconds
    long tv_nsec;  // Nanoseconds
}
int clock_getres(clockid_t clk_id, struct timespec *res);
int clock_gettime(clockid_t clk_id, struct timespec *tp);
int clock_settime(clockid_t clk_id, const struct timespec *tp);

Псевдослучайные числа

Истинную случайность сложно имитировать с помощью детерминированной вычислительной машины, поэтому прибегают к генерации псевдослучайных чисел. Один из методов — линейный конгруэнтный генератор :

\[x_{n+1} = (ax_n + c) \mod{m}\]

\(x_0\) — начальное значение (seed).

Получается периодическая последовательность c периодом не превышающем \(m\).

Числа \(a\), \(c\) и \(m\) специально подбираются, чтобы обеспечить видимость случайности и достаточно длинный период. В качестве \(m\) удобно взять \(2^n\) , где \(n\) — размер разрядной сетки переменной.

В стандартной библиотеке Си:

#include <stdlib.h>
int rand(void);
void srand(unsigned int seed);

Функция srand() устанавливает свой аргумент как основу (seed) для новой последовательности псевдослучайных целых чисел.

Функция rand() возвращает псевдослучайное целое число в диапазоне от нуля до RAND_MAX включительно.

Пример реализации генератора (стандарт POSIX.1-2001):

#define RAND_MAX 32767

static unsigned long next = 1;

int rand(void) {
    next = next * 1103515245 + 12345;
    return ((unsigned)(next/65536) % 32768);
}

void srand(unsigned int seed) {
    next = seed;
}

Пример использования:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void) {
    int i;
    float f;

    /* Текущее время в качестве начального значения */
    srand(time(NULL));

    /* Случайное число в диапазоне от 0 до RAND_MAX включительно */
    i = rand();
    printf("%d\n", i);

    /* Случайное число в диапазоне от 0 до 99 включительно */
    i= rand() % 100;
    printf("%d\n", i);

    int a = 100;
    int b = 999;

    /* Целое случайное число в диапазоне [a, b] */
    i = rand() % (b - a + 1) + a;
    printf("%d\n", i);

    /* Случайное число с плавающей запятой в диапазоне [0, 1] */
    f = (float) rand() / RAND_MAX;
    printf("%f\n", f);

    /* Случайное число с плавающей запятой в диапазоне [a, b] */
    f = (float) rand() / RAND_MAX * (b - a) + a;
    printf("%f\n", f);

    return 0;
}

Генераторы на основе линейного конгруэнтного метода являются предсказуемыми, поэтому их нельзя использовать в криптографии.

Современные операционные системы предоставляют генераторы случайных чисел, которые полагаются на энтропию, собранную с драйверов устройств и других источников окружающего шума. В Линукс доступ к генератору возможен с помощью системного вызова getrandom и файлов устройств /dev/urandom и /dev/random.

Функции с переменным числом аргументов

Некоторые функции допускают варьирование числа параметров:

int printf(const char *format, ...);

Для доступа к таким аргументам используются макросы из stdarg.h:

void va_start(va_list ap, last);
type va_arg(va_list ap, type);
void va_end(va_list ap);

— это не функции!

#include <stdio.h>
#include <stdarg.h>

int max_int(size_t n, ...) {
    va_list ap;
    int current, largest;

    va_start(ap, n);
    largest = va_arg(ap, int);    
    for (size_t i = 1; i < n; i++) {
        current = va_arg(ap, int);
        if (current > largest)
            largest = current;
    }
    va_end(ap);

    return largest;
}

int main(void) {
    /* Первый аргумент 4 определяет количество чисел */
    int m = max_int(4, 2, -1, 3, 1);

    printf("%d\n", m); /* 3 */

    return 0;
}

Другие способы задать количество аргументов:

  • Форматная строка. Количество извлекаемых параметров из списка переменной длины определяется по количеству специальных комбинаций символов в форматной строке.
  • Признак конца списка. Одно из возможных значений элементов списка обозначает завершение обработки, например, для указателей, может быть NULL.

Локальные и нелокальные переходы

Локальный переход с помощью goto:

#include <stdio.h>

int main(void) {
    int i = 0;
    int n = 10;

loop:
    printf("%d\n", i++);
    if (i < n)
        goto loop;

    return 0;
}

Используется редко — операторы переходов могут сильно запутать код. Следует использовать операторы цикла и другие элементы структурного программирования. Тот же цикл:

#include <stdio.h>

int main(void) {
    int i = 0;
    int n = 10;

    do {
        printf("%d\n", i++);
    } while (i < n);

    return 0;
}

Можно перейти в область видимости переменной так, что она останется неинициализироанной. Значение i неопределено при переходе по метке negative:

if (x < 0)
    goto negative;

if (y < 0) {
    int i = 5;
negative:
    printf("Negative, and i is %d\n", i);
    return;
}

Оператор goto следует использовать только в тех сложных и редких случаях, когда это действительно помогает улучшить читаемость кода. Обычно это переходы вперед из нескольких мест в функции к общему коду обработки ошибок. Переходы назад практически всегда лучше заменить оператором цикла.

Нелокальные переходы

Обычно функция возвращает управление в точку вызова. Нельзя использовать goto для выхода извне функции, но нелокальные переходы все же возможны.

#include <setjmp.h>

int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);

Первая функция позволяет установить позицию для последующего возврата, вторая — перейти в установленную точку. На практике механизм нелокальных переходов также используется редко, обычно — для обработки ошибок.

#include <setjmp.h>
#include <stdio.h>

jmp_buf env;

void f1(void);
void f2(void);

int main(void) {
    if (setjmp(env) == 0) {
        printf("setjmp returned 0\n");
    } else {
        printf("Program terminates: longjmp called\n");
        return 0;
    }

    f1();

    printf("Program terminates normally\n");    
    return 0;
}


void f1(void) {
    printf("f1 begins\n");
    f2();
    printf("f1 returns\n");
}

void f2(void) {
    printf("f2 begins\n");
    longjmp(env, 1);
    printf("f2 returns\n");
}

Вывод:

setjmp returned 0
f1 begins
f2 begins
Program terminates: longjmp called

Работа на уровне битов

Минимальной единицей доступа к памяти является байт (к значению байта можно обратиться по адресу). Напрямую установить значение некоторого бита для некоторого байта нельзя, но в Си предусмотрены операторы для манипуляции битами значений целочисленных типов.

Логические побитовые операторы

  • ~a — двоичное дополнение.

    ~0b10101000 ⇒ 0b11111111111111111111111101010111
    ~0 ⇒ 0b11111111111111111111111111111111
    ~0b11111111111111111111111111111111 ⇒ 0
    ~ (-1) ⇒ 0
    

    Следствия использования дополнительного кода:

    • ~x + 1 ​=​= -x
    • ~x ​=​= -x - 1
  • a & b — побитовое «И» («AND»).

    0b10101010 & 0b11001100 ⇒ 0b10001000
    
  • a | b — побитовое «ИЛИ» («OR»).

    0b10101010 | 0b11001100 ⇒ 0b11101110
    
  • a ^ b — побитовое исключающее «ИЛИ» («XOR»).

    0b10101010 ^ 0b11001100 ⇒ 0b01100110
    

    Свойства:

    x ^ 0 == x
    x ^ x == 0
    Коммутативность: x ^ y == y ^ x
    Ассоциативность: x ^ (y ^ z) == (x ^ y) ^ z
    
    • XOR-обмен:

      x = x ^ y;
      y = y ^ x;
      x = x ^ y;
      
    • шифрование (одноразовый блокнот)
    • XOR-связный список

Битовые флаги

Пример: функция open() API POSIX:

/* Сброс маски процесса, т. к. open использует эффективный режим,
   вычисляемый как mode & ~umask. */
umask(0)

/* Режим доступа: все права для владельца, группы, и остальных. */
int mode = S_IRWXU | S_IRWXG | S_IRWXO;

/* Убрать биты записи для группы и остальных. */
mode &= ~(S_IWGRP | S_IWOTH);
/* Или можно было установить маску:
   umask(S_IWGRP | S_IWOTH); */

/* Комбинация флагов:
   создать файл, только если не существует, открыть для записи. */
int fd = open(path1, O_CREAT | O_EXCL | O_WRONLY, mode);

/* Был ли задан бит записи для владельца? */
if (mode & S_IWUSR) /* истина */
    …;

Операторы сдвига

  • << — сдвиг влево:

    5 << 2 ⇒ 20

  • >> — сдвиг вправо

    5 >> 2 ⇒ 1

Сдвиг влево на 1 бит эквивалентен умножению на 2:

5 << 3 == 5 * 2*2*2
-10 << 4 == -10 * 2*2*2*2

Сдвиг вправо сохраняет знак для знаковых типов и эквивалентен делению на 2, но с округлением в меньшую сторону а не к 0:

19 >> 2 == 4
20 >> 2 == 5
21 >> 2 == 5
-1 >> 1 == -1
-5 >> 1 == -3
-5 / 2 == -2

Приемы со сдвигами:

  • Установка j-го бита i:

    i |= 1 << j;
    
  • Сброс j-го бита значения i:

    i &= ~(1 << j);
    
  • Проверка j-го бита значения i:

    if (i & (1 << j)) …;
    
  • Упаковка значений:

    /* Дата первого релиза GCC */
    unsigned int d = 22;
    unsigned int m = 3;
    unsigned int y = 1987;
    unsigned int date = ((y << 4) + m) << 5) + d;
    

    Обратно:

    d = date % 32;
    m = (date >> 5) % 16;
    y = date >> 9;
    

Обработка ошибок

Некоторые функции стандартной библиотеки указывают на ошибку, сохраняя ее код в специальной глобальной переменной errno.

Часто явная проверка errno не требуется, т. к. появление ошибки определяется по возвращаемому значению функции, но можно проверить и явно:

#include <math.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <locale.h>
#include <string.h>

int main(void) {
    double x, y;

    setlocale(LC_ALL, "");

    scanf("%lf", &x);

    /* Необходимо обнулить: значение могло измениться выше в
       результате вызовов setlocale и scanf */
    errno = 0;
    y = sqrt(x);    
    if (errno != 0) {
        perror("sqrt");
        /* Или можно получить и использовать строку
           с описанием ошибки:
           fprintf(stderr, "sqrt: %s\n", strerror(errno));
        */
        return EXIT_FAILURE;
    }

    printf("%lf\n", y);

    return EXIT_SUCCESS;
}

Проверки assert

#include <assert.h>
void assert(scalar expression);

Макрос assert аварийно завершит программу с выводом диагностического сообщения, если выражение будет иметь ложное значение.

assert(0 <= i && i < 10);
a[i] = 0;

Пример сообщения:

test: test.c:13: main: Assertion `0 <= i && i < 10' failed.

Если перед включением assert.h определен макрос NDEBUG, проверки игнорируются: можно исключить влияние дополнительных проверок на производительность после завершения отладки.

Библиотека SDL

Simple DirectMedia Layer (сокращенно SDL) – это свободная кроссплатформенная библиотека, реализующая единый интерфейс к графической подсистеме, устройствам ввода и звуковой подсистеме, официально поддерживающая операционные системы Linux, Microsoft Windows и Mac OS на уровне как исходных текстов, так и скомпонованных библиотек, а платформы iOS и Android – на уровне исходных текстов.

Основная часть SDL содержит лишь весьма ограниченный, базовый набор возможностей, а дополнительные функции (такие, как графические примитивы или вывод текста) обеспечиваются расширениями библиотеки.

Следует отметить, что, начиная с версии 2.0 (для объединения всех версий SDL 2.x и отличия их от SDL 1.x часто используется обозначение SDL2), SDL распространяется по лицензии zlib, а не LGPL, что позволяет более свободно использовать ее в ком-мерческих программах.

Структурно SDL (как и большинство графических библиотек) можно рассматривать как относительно «тонкую» прослойку между конкретной прикладной программой и конкретными программными интерфейсами конкретных операционных систем для работы с соответствующей аппаратурой (например, при работе с графикой скрывать от разработчика прикладной программы особенности работы с DirectX в Microsoft Windows, Xlib в Linux и т.п.).

Основные возможности SDL – поддержка операций над двумерными плоскостями пикселей (включая создание окон), обработка событий (от клавиатуры, мыши и таймера), а также работа со звуком и абстрагирование доступа к файлам.

Для возможности вызова в программе функций из библиотеки SDL необходимо подключить заголовочный файл SDL.h

#include "SDL.h"

Данный заголовочный файл включает в себя все остальные заголовочные файлы библиотеки SDL, поэтому подключения других не требуется.

Непосредственно перед использованием функций библиотеки SDL необходимо инициализировать соответствующие ее подсистемы с помощью функции SDL_Init:

extern DECLSPEC int SDLCALL SDL_Init(Uint32 flags);

Важным является тип аргумента flags – целое число без знака, представленное 32 битами (4 байта). Для переносимости между платформами и различными компиляторами он объявлен как самостоятельный тип с помощью директив препроцессора, обеспечивающих условную трансляцию. С помощью данного параметра указывается, какие именно подсистемы библиотеки требуется инициализировать и какие глобальные режимы использовать. Для этого объявлен ряд флагов, которые можно комбинировать с помощью операции побитового ИЛИ.

для инициализации подсистемы работы с дисплеем служит флаг SDL_INIT_VIDEO

для работы с таймером – SDL_INIT_TIMER

для работы с подсистемой обработки звука – SDL_INIT_AUDIO

для инициализации всех подсистем служит специальный флаг SDL_INIT_EVERYTHING

Результат, возвращаемый функцией SDL_Init, указывает на успешность инициализации, если он равен нулю, или на ошибку инициализации в противном случае. При ошибке можно получить описание ошибки с помощью функции SDL_GetError:

extern DECLSPEC char * SDLCALL SDL_GetError(void);

Если инициализация была успешно выполнена, при последующем завершении работы программы необходимо вызвать функцию SDL_Quit (не имеющую параметров и не возвращающую никаких значений). Для гарантии вызова данной функции при любом завершении программы желательно установить ее как функцию, автоматически вызываемую при выходе из программы:

/* Cразу после вышеприведенного фрагмента. 
 * Оператор выполняется при безошибочной инициализации SDL*/
atexit(SDL_Quit);

Отображение графической информации в библиотеке SDL основано на понятии «поверхность». Поверхность логически представляет собой прямоугольную матрицу, состоящую из пикселей определенного формата, на которой можно рисовать, изменяя состояние пикселей. Набор возможных состояний пикселя определяется его форматом. Все пиксели одной поверхности имеют одинаковый формат. Поэтому его также можно считать «форматом поверхности». Возможно и более сложное использование поверхностей, например для формирования изображения наложением изображений разных поверхностей. В программе каждая поверхность представляется указателем на структуру SDL_Surface. Данный указатель возвращают функции, создающие поверхности как объекты программы, и впоследствии они используются для указания поверхности при всех операциях с ними. Основное окно программы, возможно полноэкранное, также является поверхностью, однако создающейся специальной функцией установки видеорежима SDL_SetVideoMode:

extern DECLSPEC SDL_Surface * SDLCALL 
      SDL_SetVideoMode (int width, 
                        int height, 
                        int bpp, 
                        Uint32 flags);

Она возвращает либо корректный указатель на структуру SDL_Surface, соответствующую поверхности окна программы (или всего экрана), либо NULL в случае ошибки. Это позволяет контролировать возникновение ошибок при установке видео-режима в простейшем случае опять же с помощью функции SDL_GetError:

/* в соответствующем месте объявляем указатель на поверхность: */
SDL_Surface *screen; /* ... */

/* После инициализации собственно SDL 
 * и установки atexit(SDL_Quit): */ 

screen=SDL_SetVideoMode(800,600,32,SDL_ANYFORMAT);
if (!screen)
{
   fprintf(stderr,
           "SDL mode failed: %s\n",
           SDL_GetError()); 
   return 1;
}

Для обеспечения быстрого наложения поверхностей (с использованием возможных средств аппаратного ускорения) в основной библиотеке SDL служит функция SDL_BlitSurface, которая в рассматриваемой версии библиотеки с помощью макроподстановок может считаться объявленной так (подробности можно найти в заголовочном файле SDL_video.h):

extern DECLSPEC int SDLCALL 
       SDL_BlitSurface (SDL_Surface *src, 
                        SDL_Rect *srcrect, 
                        SDL_Surface *dst, 
                        SDL_Rect *dstrect);

Первый параметр – указатель на накладываемую поверхность, второй – указатель на структуру SDL_Rect, в которой компонентами x и y задан верхний левый угол, а компонентами w и h – ширина и высота накладываемого фрагмента данной поверхности. При этом если компонентами второго параметра задается область, выходящая за пределы накладываемой поверхности, внутри функции они корректируются для предотвращения обращения за пределы ее области памяти и используются откорректированные значения.

При наложении поверхностей функцией SDL_BlitSurface может происходить как простое замещение атрибутов пикселя на целевой поверхности атрибутами пикселя накладываемой поверхности, так и учет прозрачности пикселя (значения альфа-канала), прозрачности всей поверхности и цвета, указанного как прозрачный.

При успешном выполнении функция SDL_BlitSurface возвращает значение 0, в случае ошибки –1. При успешном выполнении и отличном от NULL значении четвертого параметра в него записываются параметры фактически использованного фрагмента целевой поверхности.

Обновление отображения поверхности на экране функцией SDL_Flip(screen).

Если аппаратное обновление не поддерживается, такой вызов эквивалентен вызову функции частичного обновления поверхности SDL_UpdateRect с нулевыми параметрами, определяющими полное обновление отображения поверхности:

SDL_UpdateRect(screen, 0, 0, 0, 0);

Рассмотрим код лаборатрной работы №7

#include <stdio.h>
#include 
#include 

#include 

#define VXD 5
#define VYD 5


/* Описание экрана игры */
typedef struct _gamescreen {
    /* Поверхность отрисовки */
    SDL_Surface *sprite;
} gamescreen;


/* Описание управляемого пользователем корабля */
typedef struct _spaceship {
    /* Поверхность отрисовки */
    SDL_Surface *sprite;
    /* Координаты корабля */
    int x;
    int y;
    /* Проекции скорости корабля */
    int vx;
    int vy;
} spaceship;


/* Ресурсы и состояние игры  */
typedef struct _game {
    /* Экран игры */
    gamescreen *screen;
    /* Корабль пользователя */
    spaceship *ship;
} game;

/**
 * Инициализирует игру
 * @returns g указатель на структуру состояния игры
 */
game *init();

/**
 * Инициализирует игру
 * @param g указатель на структуру состояния игры
 */
void run(game *g);

/**
 * Отрисовывает объекты в новой позиции
 * @param g указатель на структуру состояния игры
 */
void draw(game *g);


/**
 * Основная программа
 */
int main()
{
    /* Инициализируем игру */
    game *g = init();

    /* Запускаем цикл обработки событий игры */
    run(g);

    return 0;
}


/**
 * Инициализирует игру
 * @returns g указатель на структуру состояния игры
 */
game *init()
{
    /* Создаем структуру представления состояния игры */
    game *g;
    g = (game *) malloc(sizeof(game));
    if (g == NULL) {
        fprintf(stderr, "Not enough memory!");
        exit(EXIT_FAILURE);
    }

    /* Инициализируем библиотеку SDL, используем только видеоподсистему */
    if (SDL_Init(SDL_INIT_VIDEO) < 0) {
        fprintf(stderr, "Couldn't initialize SDL: %s\n", SDL_GetError());
        exit(EXIT_FAILURE);
    }

    /* Регистрируем обработчик завершения программы */
    atexit(SDL_Quit);

    /* Выделяем память для структуры представления экрана */
    g->screen = (gamescreen *) malloc(sizeof(gamescreen));
    if (g->screen == NULL) {
        fprintf(stderr, "Not enough memory!");
        exit(EXIT_FAILURE);
    }    
    
    /* Инициализируем видеорежим */
    g->screen->sprite =
        SDL_SetVideoMode(1024, 768, 0, SDL_HWSURFACE | SDL_DOUBLEBUF);
    if (g->screen->sprite == NULL) {
        fprintf(stderr, "Couldn't set video mode: %s\n", SDL_GetError());
        exit(EXIT_FAILURE);
    }

    /* Выделяем память для структуры представления корабля */
    g->ship = (spaceship *) malloc(sizeof(spaceship));
    if (g->ship == NULL) {
        fprintf(stderr, "Not enough memory!");
        exit(EXIT_FAILURE);
    }

    g->ship->sprite = SDL_LoadBMP("ship.bmp");
    if (g->ship->sprite == NULL) {
        fprintf(stderr, "Couldn't load a bitmap: %s\n", SDL_GetError());
        exit(EXIT_FAILURE);
    }

    /* Устанавливаем заголовок окна */
    SDL_WM_SetCaption("Space explore", NULL);

    return g;
}


/**
 * Инициализирует игру
 * @param g указатель на структуру состояния игры
 */
void run(game * g)
{
    /* Флажок выхода */
    int done = 0;

    /* Продолжаем выполнение, пока не поднят флажок */
    while (!done) {
        /* Структура описания события */
        SDL_Event event;

        /* Извлекаем и обрабатываем все доступные события */
        while (SDL_PollEvent(&event)) {
            switch (event.type) {
            /* Если клавишу нажали */
            case SDL_KEYDOWN:
                switch (event.key.keysym.sym) {
                case SDLK_LEFT:
                    g->ship->vx += -VXD;
                    break;
                case SDLK_RIGHT:
                    g->ship->vx += VXD;
                    break;
                case SDLK_UP:
                    g->ship->vy += -VYD;
                    break;
                case SDLK_DOWN:
                    g->ship->vy += VYD;
                    break;
                case SDLK_ESCAPE:
                    done = 1;
                    break;
                }
                break;
            /* Если клавишу отпустили */
            case SDL_KEYUP:
                switch (event.key.keysym.sym) {
                case SDLK_LEFT:
                    g->ship->vx += VXD;
                    break;
                case SDLK_RIGHT:
                    g->ship->vx += -VXD;
                    break;
                case SDLK_UP:
                    g->ship->vy += VYD;
                    break;
                case SDLK_DOWN:
                    g->ship->vy += -VYD;
                    break;
                default:
                    break;
                }
                break;
            /* Если закрыли окно */
            case SDL_QUIT:
                done = 1;
                break;
            default:
                break;
            }
        }

        g->ship->x += g->ship->vx;
        g->ship->y += g->ship->vy;
        draw(g);
        SDL_Delay(10);
    }
}


/**
 * Отрисовывает объекты в новой позиции
 * @param g указатель на структуру состояния игры
 */
void draw(game * g)
{
    /* Прямоугольники, определяющие зону отображения */
    SDL_Rect src, dest;

    /* Корабль отображаем целиком */
    src.x = 0;
    src.y = 0;
    src.w = g->ship->sprite->w;
    src.h = g->ship->sprite->h;

    /* в новую позицию */
    dest.x = g->ship->x;
    dest.y = g->ship->y;
    dest.w = g->ship->sprite->w;
    dest.h = g->ship->sprite->h;

    /* Выполняем отображение */
    SDL_BlitSurface(g->ship->sprite, &src, g->screen->sprite, &dest);
    
    /* Отрисовываем обновленный экран */
    SDL_Flip(g->screen->sprite);
}

Модификаторы вывода функции printf

Функция printf() возвращает число реально выведенных символов. Если функция возвратит отрицательное значение, то это будет свидетельствовать о наличии ошибки.

На спецификации формата могут воздействовать модификаторы, задающие ширину поля, точность и признак выравнивания по левому краю. Целое значение, расположенное между знаком % и командой форматирования, играет роль спецификации минимальной ширины поля. Наличие этого спецификатора приводит к тому, что результат будет заполнен пробелами или нулями, чтобы выводимое значение занимало поле, ширина которого не меньше заданной минимальной ширины. Если длина выводимого значения (строки или числа) больше этого минимума, оно будет выведено полностью несмотря на превышение минимума. По умолчанию в качестве заполнителя используется пробел. Для заполнения нулями перед спецификацией ширины поля нужно поместить 0. Например, спецификация формата %05d дополнит нулями выводимое число, в котором менее пяти цифр, чтобы общая длина равнялась 5 символам.

Действие модификатора точности зависит от кода формата, к которому он применяется. Чтобы добавить модификатор точности, поставьте за спецификацией ширины поля десятичную точку, а после нее — требуемое значение точности. Для форматов a, A, e, E, f и F модификатор точности определяет число выводимых десятичных знаков. Например, спецификация формата %10.4f обеспечит вывод числа с четырьмя знаками после запятой в поле шириной не меньше десяти символов. Если модификатор точности применяется к коду формата g или G, то он определяет максимальное число выводимых значащих цифр. Применительно к целым, модификатор точности задает минимальное количество выводимых цифр. При необходимости перед числом будут добавлены нули.

Если модификатор точности применяется к строкам, число, следующее за точкой, задает максимальную длину поля. Например, спецификация формата %5.7s выведет строку длиной не менее пяти, но не более семи символов. Если выводимая строка окажется длиннее максимальной длины поля, конечные символы будут отсечены.

По умолчанию все выводимые значения выравниваются по правому краю: если ширина поля больше выводимого значения, оно будет выровнено по правому краю поля. Чтобы установить выравнивание по левому краю, нужно поставить знак "минус" сразу после знака %. Например, спецификация формата %-10.2f обеспечит выравнивание вещественного числа с двумя десятичными знаками в 10-символьном поле по левому краю.

Существуют два модификатора формата, позволяющие функции printf() отображать короткие и длинные целые. Эти модификаторы могут применяться к спецификаторам типа d, i, о, u, x и X. Модификатор l уведомляет функцию printf() о длинном типе значения. Например, спецификация %ld означает, что выводится длинное целое число. Модификатор h сообщает функции printf(), что нужно вывести число короткого целого типа. Следовательно, строка %hu означает, что выводимое данное имеет тип short unsigned int.

Таблица 13.2. Спецификаторы формата функции printf()
КодФормат
%aВыводит шестнадцатеричное число в форме 0xh.hhhhp+d (только C99)
%AВыводит шестнадцатеричное число в форме 0Xh.hhhhP+d (только C99)
Символ
%dДесятичное целое число со знаком
%iДесятичное целое число со знаком
%eЭкспоненциальное представление числа (в виде мантиссы и порядка) (e на нижнем регистре)
%EЭкспоненциальное представление числа (в виде мантиссы и порядка) (E на верхнем регистре)
%fДесятичное число с плавающей точкой
%FДесятичное число с плавающей точкой (только C99; если применяется к бесконечности или к нечисловому значению, то выдает надписи INF, INFINITY или NAN на верхнем регистре. Спецификатор %f выводит их эквиваленты на нижнем регистре.)
%gИспользует более короткий из форматов %e или %f
%GИспользует более короткий из форматов %E или %F
%oВосьмеричное число без знака
%sСимвольная строка
%uДесятичное целое число без знака
%xШестнадцатеричное без знака (строчные буквы)
%XШестнадцатеричное без знака (прописные буквы)
%pВыводит указатель
%nСоответствующий аргумент должен быть указателем на целое число. (Этот спецификатор указывает, что в целочисленной переменной, на которую указывает ассоциированный с данным спецификатором указатель, будет храниться число символов, выведенных к моменту обработки спецификации %n.)
%%Выводит знак процента

При использовании современного компилятора, поддерживающего добавленные в 1995 году средства работы с двухбайтовыми символами, можно к спецификации с применить модификатор l, чтобы указать на использование двухбайтовых символов. Модификатор l можно также использовать с командой формата s для вывода строки двухбайтовых символов.

Кроме того, модификатор l можно поставить перед командами форматирования вещественных чисел a, A, e, E, f, F, g и G. В этом случае он уведомит о выводе значения типаlong double.

Команда n сохраняет в целой переменной, указатель на которую задан в списке аргументов, число символов, которые были записаныв поток вывода к моменту обнаружения спецификатора n. Например, следующий фрагмент программы после строки

"Это тест"
выведет число 8.
int i;
printf("Это тест%n", &i);
printf("%d", i);

Чтобы обозначить, что соответствующий аргумент указывает на длинное целое, к спецификации n можно применить модификатор l. Для указания на короткое целое примените к спецификации n модификатор h.

Символ # при использовании с некоторыми кодами формата функции printf() приобретает специальное значение. Поставленный перед кодами a, A, g, G, f, e и E, он гарантирует наличие десятичной точки даже в случае отсутствия десятичных цифр. Если поставить символ # перед кодами формата x и X, то шестнадцатеричное число будет выведено с префиксом 0x. Если же его поставить перед кодами формата o и O, то восьмеричное число будет выведено с префиксом 0. Символ # нельзя применять ни к каким другим спецификациям формата.

Спецификации минимальной ширины поля и точности могут задаваться не константами, а аргументами функции printf(). Для этого в строке форматирования используется символ "звездочка" (*). При сканировании строки форматирования функции printf() каждый символ * будет сопоставляться с соответствующими аргументами в порядке их следования.

В версии C99 для использования в функции printf() добавлены модификаторы формата hh, ll, j, z и t. Модификатор hh можно применять к спецификаторам преобразования d, i, o, u, x, X и n. Он означает, что соответствующий аргумент является значением типа signed char или unsigned char, а в случае спецификации n — указателем на переменную типа signed char. Модификатор ll также можно применять к спецификаторам преобразования d, i, о, u, x, X и n. Он означает, что соответствующий аргумент является значением типа signed long long int или unsigned long long int, а в случае спецификатора n — указателем на переменную типа long long int. Версия C99 также позволяет применять модификатор l к спецификаторам преобразования чисел с плавающей точкой a, A, e, E, f, F, g и G, но это не дает никакого эффекта.

Применение модификатора формата j к спецификаторам преобразования d, i, с, u, x, X и n устанавливает для соответствующего аргумента тип intmax_t или uintmax_t. Эти типы объявлены в заголовке <stdint.h> и служат для хранения целых самой большой разрядности.

Применение к спецификаторам преобразования d, i, o, u, x, X и n модификатора формата z устанавливает для соответствующего аргумента тип size_t. Этот тип объявлен в заголовке <stddef.h> и служит для хранения результата выполнения оператора sizeof.

Применение к спецификаторам преобразования d, i, o, u, x, X и n модификатора формата t устанавливает для соответствующего аргумента тип ptrdiff_t. Этот тип объявлен в заголовке <stddef.h> и служит для хранения значения разности между двумя указателями.

Пример:

#include <stdio.h>
int main(void)
{
  /* Этот фрагмент печатает строку "это тест            "
     которая выравнивается по левому краю поля шириной в 20 символов.
  */
  printf("%-20s", "это тест");
  /* Этот фрагмент печатает в поле шириной в 10 символов число
     с плавающей точкой с тремя десятичными разрядами после запятой.
     В результате получится "    12.235".
  */
  printf("%10.3f", 12.234657);
  return 0;
}

Препроцессор

Макросы

Все идентификаторы, определяемые с помощью директив #define, которые предполагают замену на определенную последовательность символов, еще называют макросами.

Макросы позволяют определять замену не только для отдельных символов, но и для целых выражений:

#include <stdio.h>
 
#define HELLO printf("Hello World! \n")
#define FOR for(int i=0; i<4; i++)
 
int main(void)
{
    FOR HELLO;
    return 0;
}

Макрос HELLO определяет вывод на консоль строки "Hello World! \n". А макрос FOR определяет цикл, который отрабатывает 4 раза. И итоге после обработки препроцессора функция main будет выглядеть следующим образом:

int main(void)
{
    for(int i=0; i<4; i++) printf("Hello World! \n");
    return 0;
}

То есть данный код 4 раза выведет на консоль строку "Hello World! \n".

Подобные определения директивы #define имеют один недостаток, последовательность символов, которая используется директивой фиксирована. Например, здесь везде, где встретится в исходном коде идентификатор HELLO, выводится строка "Hello World!". Но что, если мы динамически хотим передавать строку, то есть строка может быть любой. В этом случае мы можем задать макроопределение с параметрами в следующей форме:

#define имя_макроса(список_параметров) последовательность_символов

Список_параметров здесь это список идентификаторов, разделенных запятыми. Между именем макроса и открывающей скобкой не должно быть пробелов.

Для обращения к макросу применяется конструкция:

имя_макроса(список_аргументов)

Список_аргументов - это набор значений, которые передаются для каждого параметра макроса.

Например, возьмем банальную операцию, по выводу чисел на консоль и попробуем сократить ее с помощью макросов:

#define print(a) printf("%d \n", a)

Здесь print - это имя макроса или идентификатор, после которого в скобках указан параметр a. Этот параметр будет представлять любое целое число. И любой вызов макроса print будет заменяться на строку printf("%d \n", a). Посмотрим, это будет выглядеть на примере:

#include <stdio.h>
#define print(a) printf("%d \n", a)
 
int main(void)
{
    int x = 10;
    print(x);
    int y =20;
    print(y);
    print(22);
    return 0;
}

Или более сложный пример: определим макрос swap(t,x,y), который обменивает местами значения двух аргументов типа t:

#include <stdio.h>
 
#define t int
#define swap(t, x, y) { t temp = x; x = y; y=temp;}
 
int main(void)
{
    t x = 4;
    t y = 10;
    swap(t, x, y)
    printf("x=%d \t y=%d", x, y);
    return 0;
}

Макрос swap применяет блок для обмена значениями. Причем данный макрос фактически универсален: нам неважно, какой тип у переменных x и y.

Или еще один пример - нахождение минимального значения:

#include <stdio.h>
#define min(a,b) (a < b ? a : b)
 
int main(void)
{
    int x = 23;
    int y = 14;
    int z = min(x,y);
    printf("min = %d", z);  // min = 14
    return 0;
}

То есть в данном случае после работы препроцессора вместо строки int z = min(x,y); мы получим строку:

int z = (x < y ? x : y);

Препроцессорные операции

При обработки исходного кода препроцессор может выполнять две операции: # и ##.

Операция # позволяет заключать текст параметра, который следует после операции, в кавычки:

#include <stdio.h>
#define print_int(n) printf(#n"=%d \n",n);
 
int main(void)
{
    int x = 23;
    print_int(x);       // x=23
    int y = 14;
    print_int(y);       // y=14
    int number = 203;
    print_int(number);  // number=203
    return 0;
}

Директива ## позволяет объединять две лексемы:

#include <stdio.h>
#define print(a,b,c) printf("%d", a##b##c);
 
int main(void)
{
    print(2, 81, 34);   // 28134
    return 0;
}

Здесь склеиваются три числа, которые передаются в макрос print. Или аналогичный пример:

#include <stdio.h>
#define unite(a,b,c) a##b##c;
 
int main(void)
{
    int x = unite(2, 81, 34);   // 28134
    printf("%d \n", x);
    return 0;
}

Условная компиляция

Директивы условной компиляции позволяют в зависимости от условий добавить добавить в файл определенный код.

Прежде всего это такие директивы как #if/#else/#endif, действие которых напоминает условную конструкцию if:

#if условие
исходный_код
#endif

Если условие возвращает ненулевое значение (то есть оно истинно), то в итоговый исходный файл вставляется исходный код, который расположен между директивами #if и #endif:

#include <stdio.h>
#define N 22
 
int main(void)
{
#if N==22
    printf("N=22");
#endif
    return 0;
}

Директива #else позволяет задать альтернативый код, который компилируется, если условие не верно:

#include <stdio.h>
#define N 22
  
int main(void)
{
#if N==22
    printf("N=22");
#else
    printf("N is undefined");
#endif
    return 0;
}

С помощью директивы #elif можно проверять дополнительные условия:

#include <stdio.h>
#define N 24
 
int main(void)
{
#if N==22
    printf("N = 22");
#elif N==24
    printf("N=24");
#else
    printf("N is undefined");
#endif
    return 0;
}
#ifdef

С помощью директивы #ifdef можно проверять, определен ли идентификатор, и если он определен, вставлять в исходный код определенный текст:

#include <stdio.h>
#define DEBUG
 
int main(void)
{
#ifdef DEBUG
    printf("Debug mode");
#endif
    return 0;
}

Обратным действием обладает директива #ifndef - она включает текст, если идентификатор не определен:

#include <stdio.h>
//#define DEBUG
 
int main(void)
{
#ifndef DEBUG
    printf("Production mode");
#else
    printf("Debug mode");
#endif
    return 0;
}

Если нам одновременно надо проверить значения двух идентификаторов, то можно использовать специальный оператор defined:

#include <stdio.h>
#define BETA
#define DEBUG
 
int main(void)
{
#if defined DEBUG && !defined BETA
    printf("debug mode; final version");
#elif defined DEBUG && defined BETA
    printf("debug mode; beta version");
#else
    printf("undefined mode");
#endif
    return 0;
}

Почему макро не рекомендуются к использованию

Рассмотрим пример из книги Скотта Майерса:
#define max(a,b) ((a) > (b) ? (a) : (b))
// ...
int a = 5, b= 0
max(++a, b);       // a увеличится дважды
max(++a, b+10);    // a увеличится один раз
в этом случае макрос развернется из ((a) > (b) ? (a) : (b)) в ((++a) > (b+10) ? (++a) : (b+10)), что в двух приведенных случаях приведет к его разной работе. Так же если из макроса убрать необязательные скобки не ясно как без них развернется выражение: x = max(a, b) + 10; - x = a > b ? a : b+10;

Так же в макросах нет контроля типов.

Нет ограничения по области видимости.

Макросы сложно отлаживать

Из за того что макросы могут оказаться внутри выражения и как там раскроются их нетривиальные параметры не понятно. Рассмотрим ещё один пример:

#include <iostream>

#define mymax(a,b) a>b?a:b
#define mul(a,b) a*b

int main() 
{

  std::cout << mymax(5, 10) << std::endl; // Ошибка компиляции
  std::cout << mul(5+5, 10) << std::endl; // Неожиданно на выходе 55, а не 100

  return 0;
}

Преобразование строк в числа и чисел в строки

Нередко в программах встречается ситуация, когда надо преобразовать число в строку или строку в число. Для этой цели в стандартной библиотеке языка С определены функции strtol() и snprintf().

Из строки в число: strtol

Функция strtol() преобразует строку в число типа long int. Функция определена в заголовочном файле stdlib.h и имеет следующий прототип:

long strtol(const char *restrict str, char **restrict str_end, int base);

Параметры функции

str - строка с числом, которое надо преобразовать в числовой тип. Ключевое слово restrict указывает компилятору оптимизировать код и что никакой другой параметр не будет указывать на адрес данного параметра.

str_end - указатель на последний символ строки. Данный параметр можно игнорировать, передавая ему значение NULL

base - основание, система исчисления, в которую надо преобразовать данные (значение от 2 до 36).

Результатом функции является преобразованное число типа long.

Например, преобразуем строку в число в десятичной системе:

#include <stdio.h>
#include <stdlib.h>
 
int main(void)
{
    const char * str = "24 flowers";
    long result = strtol(str, NULL, 10);
    printf("Result: %ld\n", result);    // Result: 24
    return 0;
}

В примере выше второй параметр функции никак не использовался - мы ему передавали значение NULL, и функция нормально работала. Однако он может быть полезен, если нам надо получить остаток строки, которая идет после числа:

#include <stdio.h>
#include <stdlib.h>
 
int main(void)
{
    const char * str = "24 flowers";
    char* str_end;
    long result = strtol(str, &str_end, 10);
    printf("Result: %ld\n", result);                // Result: 24
    printf("Rest of the string:%s\n", str_end);     // Rest of the string: flowers
    return 0;
}

Из числа в строку: snprintf

Функция snprintf() преобразует число в отформатированную строку. Функция определена в заголовочном файле stdio.h и имеет следующий прототип:

int snprintf(char *restrict str_buffer, 
             size_t buffer_size, 
             const char *restrict format, ... );

Параметры функции

str_buffer - строка, в которую помещается преобразованное число.

buffer_size - максимальное количество символов строки. Функция записывает в строку buffer-size - 1 байт и добавляет концевой нулевой байт

format - задает формат преобразования в строку.

При успешном преобразовании функция возвращает количество символов, записанных в строку (исключая концевой нулевой байт). При неудачном преобразовании возвращается отрицательное число.

Пример преобразования:

#include <stdio.h>
 
int main(void)
{
    int number = 354;
    char str [10];
    snprintf(str, sizeof str, "%d", number);
    printf("Result: %s\n", str);        // Result: 354
    return 0;
}

При этом строка форматирования может содержать множество параметров:

#include <stdio.h>
 
int main(void)
{
    int count = 3;
    double price = 79.99;
    char str [50];
    snprintf(str, sizeof str, "Count: %d \tPrice: %.2f", count, price);
    printf("%s\n", str);        // Count: 3        Price: 79.99
    return 0;
}

Что можно посмотреть ещё:

atof
atoi
atol
atoll
strtod
strtof
strtold
strtoll
strtoul
strtoull

Запуск Python из программы на Си

Язык программирования Python обладает большими возможностями, имеет богатый функционал и библиотеки, написание которых на языке Си может занять продолжительное время, особенно это касается сферы машинного обучения. И было бы неплохо просто взять готовую функциональность на Python и инкорпорировать в программу на C. И Python позволяет это сделать, предоставляя такую функциональность, как Embedded Python (встраиваемый Python). То есть мы можем встроить в программу на C интерпретатор Python и выполнять в программе на C код на языке Python.

Работа в интерпретаторе

Самое простое, загрузка интерпретатора Python и работа в нем. Для работы необходимо подключить заголовочный файл:

 #include <Python.h>
Загружаем интерпретатор:
Py_Initialize();
Далее идет блок работы с Python, например:
PyRun_SimpleString("print('Hello!')");
Выгружаем интерпретатор:
Py_Finalize();
Полный пример:
#include <Python.h>

void
main() {
    // Загрузка интерпретатора Python
    Py_Initialize();
    // Выполнение команды в интерпретаторе
    PyRun_SimpleString("print('Hello!')");
    // Выгрузка интерпретатора Python
    Py_Finalize();
}
Как компилировать и запустить:
gcc simple.c $(python3-config --includes --ldflags) -o simple && ./simple
Hello!
Пример вызова функции из файла Python: simple.c
#include <Python.h>

void 
python() {
    // Загрузка интерпретатора Python
    Py_Initialize();

    // Выполнение команд в интерпретаторе
    // Загрузка модуля sys
    PyRun_SimpleString("import sys");
    // Подключаем наши исходники python
    PyRun_SimpleString("sys.path.append('./src/python')");
    PyRun_SimpleString("import simple");
    PyRun_SimpleString("print(simple.get_value(2))");
    PyRun_SimpleString("print(simple.get_value(2.0))");
    PyRun_SimpleString("print(simple.get_value(\"Hello!\"))");

    // Выгрузка интерпретатора Python
    Py_Finalize();  
}

void
main() {
    puts("Test simple:");

    python();
}
simple.py
#!/usr/bin/python3

def get_value(x):
    return x
Так же можно выполнить всю программу:
#include <Python.h>

int main()
{
    char filename[] = "main.py";

    Py_Initialize();

    FILE* fp = fopen(filename, "rb");   // открываем файл и
    if(fp)
        PyRun_SimpleFile(fp, filename); // выполняем программу python

    Py_Finalize();
    return 0;
}
Или построить график через matplotlib:
#include <Python.h>

int main()
{
    Py_Initialize();
    PyRun_SimpleString(
"import numpy as np\n"
"import matplotlib.pyplot as plt\n"
"x = np.array(range(0, 8))\n"
"y = eval('2 * x + 1')\n"
"plt.title('y = 2x + 1')\n"
"plt.xlabel('x')\n"
"plt.ylabel('y')\n"
"plt.plot(x,y)\n"
"plt.show()");
    Py_Finalize();
    return 0;
}

Но это простые и неинтересные вещи, мы не получаем результат выполнения...

Об этом можно почитать тут или посмотреть примеры кода тут.

Запуск ASM из программы на Си

Пример использование ассемблерной вставки в код программы на Си

#include <stdio.h>

int main ()
{
  int res = 0, a = 105, m = 5, rand = 0;

  __asm__ volatile( "imull %%ecx, %%eax\n" /* ассемблерная вставка */
                    "rdrand %%ebx"
                    : "=a" (res), /* выходные операнды */
                      "=b" (rand),
                      "+c" (m)    /* и параметры */
                    : "a" (a)
                    : "memory", "cc" );

  printf("%d\n", res);
  printf("%u\n", rand);

  return 0;
}

Rdrand это инструкция процессоров, относящихся к архитектуре x86, для генерации случайного числа при помощи внутреннего генератора случайных чисел. Rdrand является опциональным расширением набора инструкций x86-64 и IA-32. В процессорах Intel доступна, начиная с архитектуры Ivy Bridge, в процессорах AMD — начиная с модели Ryzen. Данный генератор случайных чисел соответствует стандартам безопасности и криптографическим стандартам, таким как NIST SP800-90, FIPS 140-2, и ANSI X9.82.

Intel Secure Key, также известный как Bull Mountain, — условное название Intel для инструкции rdrand и реализующего её аппаратного генератора случайных чисел (RNG). Intel называет их ГСЧ и «цифровой генератор случайных чисел». Генератор использует встроенный в процессор источник энтропии.

Для проверки поддержки процессором RDRAND можно использовать инструкцию CPUID. При наличии поддержки бит 30 регистра ECX оказывается установлен после вызова функции 01H инструкции CPUID.

C++ и другие производные и конкурирующие языки

C++ — компилируемый, статически типизированный язык программирования общего назначения.

Поддерживает такие парадигмы программирования, как процедурное программирование, объектно-ориентированное программирование, обобщённое программирование. Язык имеет богатую стандартную библиотеку, которая включает в себя распространённые контейнеры и алгоритмы, ввод-вывод, регулярные выражения, поддержку многопоточности и другие возможности.

C++ сочетает свойства как высокоуровневых, так и низкоуровневых языков.

В сравнении с языком C — наибольшее внимание уделено поддержке объектно-ориентированного и обобщённого программирования.

Область его применения включает создание операционных систем, разнообразных прикладных программ, драйверов устройств, приложений для встраиваемых систем, высокопроизводительных серверов, а также компьютерных игр. Существует множество реализаций языка C++, как бесплатных, так и коммерческих и для различных платформ.

C++ оказал огромное влияние на другие языки программирования, в первую очередь на Java и C#.

Синтаксис C++ унаследован от языка C. Изначально одним из принципов разработки было сохранение совместимости с C. Тем не менее C++ не является в строгом смысле надмножеством C; множество программ, которые могут одинаково успешно транслироваться как компиляторами C, так и компиляторами C++, довольно велико, но не включает все возможные программы на C.

Язык возник в начале 1980-х годов, когда сотрудник фирмы Bell Labs Бьёрн Страуструп придумал ряд усовершенствований к языку C под собственные нужды (задачи теории очередей в приложении к моделированию телефонных вызовов).

Язык C, будучи базовым языком системы UNIX, на которой работали компьютеры Bell, является быстрым, многофункциональным и переносимым. Страуструп добавил к нему возможность работы с классами и объектами. В результате практические задачи моделирования оказались доступными для решения как с точки зрения времени разработки, так и с точки зрения времени вычислений. В первую очередь в C были добавлены классы (с инкапсуляцией), наследование классов, строгая проверка типов, inline-функции и аргументы по умолчанию. Ранние версии языка, первоначально именовавшегося «C with classes» («Си с классами»), стали доступны с 1980 года.

К 1983 году в язык были добавлены новые возможности, такие как виртуальные функции, перегрузка функций и операторов, ссылки, константы, пользовательский контроль над управлением свободной памятью, улучшенная проверка типов и новый стиль комментариев (//). Получившийся язык уже перестал быть просто дополненной версией классического C и был переименован из C с классами в «C++».

Никто не обладает правами на язык C++, он является свободным.

Стандарт C++ состоит из двух основных частей: описание ядра языка и описание стандартной библиотеки (только в 1998 году язык стал стандартизированным).

  • C++ поддерживает как комментарии в стиле C (/* комментарий */), так и однострочные (//)
  • Спецификатор inline для функций. Функция, определённая внутри тела класса, является inline по умолчанию. Данный спецификатор является подсказкой компилятору и может встроить тело функции в код вместо её непосредственного вызова.
  • Квалификаторы const и volatile. В отличие от С, где const обозначает только доступ на чтение, в C++ переменная с квалификатором const должна быть инициализирована. volatile используется в описании переменных и информирует компилятор, что значение данной переменной может быть изменено способом, который компилятор не в состоянии отследить. Для переменных, объявленных volatile, компилятор не должен применять средства оптимизации, изменяющие положение переменной в памяти (например, помещающие её в регистр) или полагающиеся на неизменность значения переменной в промежутке между двумя присваиваниями ей значения.
  • Для работы с памятью введены операторы new, new[], delete и delete[]. В отличие от библиотечных malloc и free, пришедших из C, данные операторы производят инициализации объекта. Для классов — это вызов конструктора, для POD-типов инициализацию можно либо не проводить(new Pod;) либо провести инициализацию нулевыми значениями (new Pod(); new Pod {};).
  • Пространства имён (namespace).
    namespace Foo
    {
       const int x = 5;
    }
    
    const int y = Foo::x;
    
    Специальным случаем является безымянное пространство имён. Все имена, описанные в нём, доступны только в текущей единице трансляции и имеют локальное связывание. Пространство имён std содержит в себе стандартные библиотеки C++.

В C++ доступны следующие встроенные типы. Типы C++ практически полностью повторяют типы данных в C:

  • символьные: char, wchar_t (char16_t и char32_t, в стандарте C++11);
  • целочисленные знаковые: signed char, short int, int, long int (и long long, в стандарте C++11);
  • целочисленные беззнаковые: unsigned char, unsigned short int, unsigned int, unsigned long int (и unsigned long long, в стандарте C++11);
  • с плавающей точкой: float, double, long double;
  • логический: bool, имеющий значения либо true, либо false.

C++ добавляет к C объектно-ориентированные возможности. Он вводит классы, которые обеспечивают три самых важных свойства ООП: инкапсуляцию, наследование и полиморфизм.

В стандарте C++ под классом подразумевается пользовательский тип, объявленный с использованием одного из ключевых слов class, struct или union, под структурой подразумевается класс, определённый через ключевое слово struct, и под объединением подразумевается класс, определённый через ключевое слово union. В зависимости от использованного ключевого слова меняются также и некоторые свойства самого класса. Например, в классе, объявленным через struct, члены без вручную прописанного модификатора доступа будут по умолчанию иметь публичный уровень доступа, а не приватный.

Стандартная библиотека C++ включает в себя набор средств, которые должны быть доступны для любой реализации языка, чтобы обеспечить программистам удобное пользование языковыми средствами и создать базу для разработки как прикладных приложений самого широкого спектра, так и специализированных библиотек. Стандартная библиотека C++ включает в себя часть стандартной библиотеки C.

Доступ к возможностям стандартной библиотеки C++ обеспечивается с помощью включения в программу (посредством директивы #include) соответствующих стандартных заголовочных файлов.

Стандартная библиотека включает в себя следующие разделы:

  • Поддержка языка. Включает средства, которые необходимы для работы программ, а также сведения об особенностях реализации. Выделение памяти, RTTI, базовые исключения, пределы значений для числовых типов данных, базовые средства взаимодействия со средой, такие как системные часы, обработка сигналов UNIX, завершение программы.
  • Стандартные контейнеры. В стандартную библиотеку входят шаблоны для следующих контейнеров: динамический массив (vector), статический массив (array), одно- и двунаправленные списки (forward_list, list), стек (stack), дек (deque), ассоциативные массивы (map, multimap), множества (set, multiset), очередь с приоритетом (priority_queue).
  • Основные утилиты. В этот раздел входит описание основных базовых элементов, применяемых в стандартной библиотеке, распределителей памяти и поддержка времени и даты в стиле C.
  • Итераторы. Обеспечивают шаблоны итераторов, с помощью которых в стандартной библиотеке реализуется стандартный механизм группового применения алгоритмов обработки данных к элементам контейнеров.
  • Алгоритмы. Шаблоны для описания операций обработки, которые с помощью механизмов стандартной библиотеки могут применяться к любой последовательности элементов, в том числе к элементам в контейнерах. Также в этот раздел входят описания функций bsearch() и qsort() из стандартной библиотеки C.
  • Строки. Шаблоны строк в стиле C++. Также в этот раздел попадает часть библиотек для работы со строками и символами в стиле C.
  • Ввод-вывод. Шаблоны и вспомогательные классы для потоков ввода-вывода общего вида, строкового ввода-вывода, манипуляторы (средства управления форматом потокового ввода-вывода в стиле C++).
  • Локализация. Определения, используемые для поддержки национальных особенностей и форматов представления (дат, валют и т. д.) в стиле C++ и в стиле C.
  • Диагностика. Определения ряда исключений и механизмов проверки утверждений во время выполнения (assert). Поддержка обработки ошибок в стиле C.
  • Числа. Определения для работы с комплексными числами, математическими векторами, поддержка общих математических функций, генератор случайных чисел.
позднее были добавлены:
  • Добавлена библиотека , реализующая общепринятые механизмы поиска и подстановки с помощью регулярных выражений.
  • Добавлена поддержка многопоточности.
  • Атомарные операции
  • unordered-варианты ассоциативных массивов и множеств.
  • Умные указатели, обеспечивающие автоматическое освобождение выделенной памяти.
#include <iostream>

// Импортируем все объявления в пространстве имён "std" в глобальное пространство имён.
using namespace std;

int main()
{
    cout << "Hello, world!" << endl;
    return 0;
}

Краткие сведения о языках Go и Rust.

Во многих направлениях ПО C по-прежнему доминирует:

  • Ядра операционных систем, например, Linux.
  • Микроконтроллеры
  • Видеокодеки
  • Общие низкоуровневые библиотеки наподобие OpenSSL
  • Инструменты командной строки Unix наподобие ls, cat и git

Можно отметить рост популярности Go (Разрабатываемый компанией Google) и Rust (поддерживаемый корпорацией Mozilla). Наблюдается, что некоторые стандартные инструменты, в прошлом обычно создававшиеся на C или C++, теперь переписываются на Go или Rust. Например, появляется множество инструментов командной строки, написанных на одном из этих языков. На Rust пишутся даже игровые движки.

Rust и Go - два современных языка, получивших значительное распространение в последние годы, каждый из которых обладает своими уникальными достоинствами и недостатками.

Производительность является важнейшим фактором при выборе языка для разработки, поскольку она может напрямую влиять на отзывчивость и масштабируемость ваших приложений. Rust и Go имеют различные подходы к достижению высокой производительности, и понимание этих различий жизненно важно для правильного выбора для вашего конкретного случая использования.

Классифицируем языки:

C, C++, Rust - высокопроизводительные языки.

Rust, Golang, Java, Python - языки с автоматическим управлением памятью.

Rust

Rust - это язык системного программирования, в котором особое внимание уделяется безопасности, параллельности и производительности. Он разработан для обеспечения низкоуровневого контроля над системными ресурсами и памятью, подобно таким языкам, как C и C++. Rust достигает высокой производительности за счет абстракций с нулевыми затратами, что означает, что абстракции практически не несут накладных расходов во время выполнения. Это позволяет разработчикам писать высокоуровневый код без ущерба для производительности.

Кроме того, мощная система типов и модель владения Rust позволяют разработчикам оптимизировать использование памяти и писать эффективный код без необходимости использования сборщика мусора. В результате приложения на Rust часто занимают меньше памяти и выполняются быстрее по сравнению с языками со сборщиком мусора.

Go

Go, также известный как Golang, - это статически типизированный компилируемый язык, который был создан в Google для повышения производительности и простоты использования крупномасштабных программных проектов. Go был разработан с учетом простоты и стремился найти баланс между простотой использования динамических языков, таких как Python и Ruby, и производительностью компилируемых языков, таких как C и C++.

Сравнение производительности

Если сравнивать Rust и Go с точки зрения производительности, то Rust, как правило, имеет преимущество благодаря низкоуровневому управлению и абстракциям с нулевыми затратами. Производительность Rust более сопоставима с такими языками, как C и C++, что делает его лучшим выбором для случаев использования, требующих максимальной производительности и эффективности использования ресурсов, таких как системное программирование или высокопроизводительные вычисления.

Особенности языка

Возможности языка программирования могут оказать значительное влияние на производительность разработчика, удобство сопровождения кода и общий успех проекта. Rust и Go обладают собственным набором возможностей языка, которые предназначены для различных стилей разработки и случаев использования.

Особенности языка Rust

Rust - это многофункциональный язык, в котором большое внимание уделяется безопасности, параллелизму и производительности. Среди особенностей языка Rust можно выделить следующие:

  • Система владения: Система владения в Rust позволяет осуществлять тонкий контроль над распределением и деаллокацией памяти, обеспечивая безопасность памяти во время компиляции без необходимости использования сборщика мусора. Эта система помогает предотвратить такие распространенные ошибки программирования, как разыменования нулевого указателя, гонки данных и ошибки использования после освобождения.
  • Сопоставление шаблонов: Сопоставление шаблонов в Rust - это мощная функция, которая позволяет создавать лаконичный и выразительный код при работе со сложными типами данных, такими как перечисления и структуры. Эта функция помогает улучшить читаемость и сопровождаемость кода.
  • Вывод типов: Система вывода типов в Rust позволяет сделать код более лаконичным за счет автоматического вывода типов переменных во многих случаях. Это помогает сократить количество шаблонов и сделать код более легким для чтения и написания.
  • Макросы: Rust поддерживает макросы, которые позволяют разработчикам определять многократно используемые фрагменты кода, которые могут быть расширены во время компиляции. Макросы могут помочь сократить дублирование кода и повысить гибкость вашей кодовой базы.

Особенности языка Go

Язык Go разработан с учетом простоты и удобства использования, подчеркивая небольшой и последовательный набор языковых функций, ориентированных на производительность и удобство сопровождения. Некоторые из примечательных особенностей языка Go включают:

  • Гороутины: Легкая модель параллелизма в Go основана на горутинах, которые похожи на потоки, но требуют меньше ресурсов. Гороутины облегчают написание параллельного и параллельного кода, повышая производительность и масштабируемость ваших приложений.
  • Каналы: Каналы - это примитив синхронизации в Go, который обеспечивает безопасное взаимодействие между горутинами. Каналы упрощают написание параллельного кода без необходимости использования сложных механизмов блокировки, улучшая читаемость и сопровождаемость кода.
  • Интерфейсы: Интерфейсы Go предоставляют мощный способ определения абстрактных типов и обеспечивают полиморфизм, позволяя создавать более гибкий и удобный в обслуживании код. В отличие от традиционного наследования, Go использует композицию и интерфейсы, что способствует многократному использованию кода и упрощает проектирование больших систем.
  • Сборка мусора: Go включает в себя сборщик мусора, который упрощает управление памятью и помогает предотвратить утечки памяти и другие распространенные ошибки программирования. Это может облегчить написание безопасного и сопровождаемого кода, особенно для разработчиков, которые только начинают программировать системы.

Сравнение особенностей языка

Если сравнивать Rust и Go с точки зрения возможностей языка, то Rust предлагает более широкий набор функций и больший контроль над системными ресурсами, что делает его хорошо подходящим для низкоуровневого системного программирования и высокопроизводительных приложений. Система владения, согласование шаблонов и макросы Rust могут дать значительные преимущества в плане безопасности и выразительности кода.

С другой стороны, Go ставит во главу угла простоту и удобство использования, что делает его отличным выбором для разработчиков, которым важны производительность и удобство сопровождения. Гороутины, каналы и интерфейсы Go позволяют легко писать параллельные и масштабируемые бэкенд-приложения с минимальным количеством кода. Кроме того, сборщик мусора в Go помогает упростить управление памятью и предотвратить распространенные ошибки программирования.

Rust и Go обладают собственным набором языковых функций, которые подходят для разных стилей разработки и случаев использования. Rust может быть лучшим выбором для разработчиков, которым требуется тонкий контроль над системными ресурсами и памятью, в то время как Go, вероятно, больше подходит для тех, кто ставит во главу угла простоту, производительность и легкость в использовании.

Параллелизм и параллелизм

Важными аспектами при разработке бэкенда являются параллелизм и параллельность, поскольку они могут значительно повлиять на производительность и масштабируемость ваших приложений. Rust и Go имеют свои собственные подходы к обработке параллелизма и параллелизма, с различными компромиссами и преимуществами.

Параллелизм и параллелизм в Rust

Rust предоставляет комбинацию потоков, каналов и async/await для параллелизма и параллелизма. Потоки в Rust похожи на потоки в других языках, позволяя выполнять несколько задач одновременно. Каналы в Rust, вдохновленные языком Go, обеспечивают безопасное взаимодействие между потоками и помогают предотвратить гонки данных и другие проблемы синхронизации.

Rust также поддерживает асинхронное программирование благодаря синтаксису async/await, который позволяет осуществлять неблокирующий ввод/вывод и эффективно решать задачи, на выполнение которых может потребоваться много времени. Экосистема асинхронного программирования Rust, включая популярные библиотеки async-std и Tokio, предоставляет мощные инструменты для создания высокопроизводительных параллельных приложений.

Параллелизм и параллелизм в Go

Подход Go к параллелизму и параллельности вращается вокруг горутин и каналов. Гороутины - это легкие, параллельные единицы выполнения, которые управляются средой выполнения Go и требуют гораздо меньше ресурсов, чем традиционные потоки. Это позволяет легко порождать тысячи или даже миллионы горутин, обеспечивая высокую параллельность и масштабируемость приложений.

Каналы в Go обеспечивают безопасное взаимодействие между горутинами, позволяя разработчикам писать параллельный код с минимальными накладными расходами на синхронизацию. Оператор select в Go позволяет обрабатывать несколько каналов одновременно, что еще больше упрощает параллельное программирование.

Сравнение параллелизма и параллелизма

Если сравнивать Rust и Go с точки зрения параллелизма и параллельности, то оба языка предоставляют мощные инструменты для создания параллельных приложений. Rust предлагает более гибкий подход с потоками, каналами и async/await, удовлетворяя широкий спектр сценариев использования и требований к производительности. Гороутины и каналы в Go позволяют легко писать параллельный код с минимальным количеством шаблонов, что может значительно повысить производительность и удобство сопровождения кода.

Rust может быть лучшим выбором для разработчиков, которым требуется тонкий контроль над параллелизмом и параллельностью или которые работают над высокопроизводительными приложениями. Go, вероятно, больше подходит для тех, кто ставит во главу угла простоту, производительность и легкость в использовании при создании параллельных бэкенд-приложений.

Безопасность памяти

Безопасность памяти - критически важный аспект разработки бэкенда, поскольку она может напрямую влиять на стабильность и безопасность ваших приложений. Rust и Go имеют различные подходы к обеспечению безопасности памяти, с разными уровнями гарантий и компромиссов.

Безопасность памяти в Rust

Rust разработан с акцентом на безопасность памяти, чтобы устранить такие распространенные ошибки программирования, как разыменования нулевого указателя, гонки данных и ошибки использования после освобождения. В Rust безопасность памяти достигается благодаря системе владения, которая обеспечивает строгие правила выделения, доступа и деаллокации памяти.

Компилятор Rust обеспечивает безопасность памяти во время компиляции, гарантируя, что небезопасный код не может быть выполнен. Это означает, что приложения на языке Rust по своей природе более безопасны и менее подвержены ошибкам, связанным с памятью, чем те, которые написаны на языках без таких гарантий.

Безопасность памяти в Go

Go предоставляет более простую модель памяти по сравнению с Rust, полагаясь на сборщик мусора для управления распределением и деаллокацией памяти. Хотя сборщик мусора в Go может помочь предотвратить утечки памяти и другие распространенные ошибки программирования, он не обеспечивает такой же уровень гарантий безопасности памяти, как система владения в Rust.

Go включает некоторые функции, помогающие смягчить проблемы безопасности памяти, такие как проверка границ для фрагментов и массивов, а также встроенный детектор гонок. Однако эти функции не обеспечивают такого же уровня безопасности, как гарантии времени компиляции в Rust.

Сравнение безопасности памяти

Если сравнивать Rust и Go с точки зрения безопасности памяти, то Rust имеет явное преимущество благодаря своей системе владения и гарантиям времени компиляции. Ориентация Rust на безопасность памяти помогает предотвратить широкий спектр ошибок и уязвимостей в безопасности, что делает его отличным выбором для внутренних приложений, требующих высокого уровня стабильности и безопасности.

Go, с другой стороны, обеспечивает более простую модель памяти с некоторыми функциями безопасности, но не предлагает такого уровня гарантий, как Rust. Этот компромисс может быть приемлемым для некоторых приложений, особенно тех, для которых простота и удобство использования приоритетнее строгой безопасности памяти.

Экосистема и библиотеки

Экосистема и доступные библиотеки для языка программирования могут значительно повлиять на скорость и простоту разработки. Развитая экосистема с широким набором библиотек и инструментов может помочь разработчикам быстро создавать и внедрять приложения, в то время как менее развитая экосистема может потребовать более тщательной разработки или обходных путей.

Экосистема и библиотеки Rust

В последние годы Rust неуклонно развивает свою экосистему и библиотечную поддержку, большое количество библиотек сторонних разработчиков доступно через менеджер пакетов Cargo. Экосистема Rust включает библиотеки для веб-разработки, баз данных, сетевых технологий и многое другое, удовлетворяя широкий спектр потребностей бэкенд-разработки.

Однако экосистема Rust все еще относительно молода по сравнению с более развитыми языками, такими как Go, и в ней может быть не так много вариантов или зрелых библиотек для определенных случаев использования. Иногда это может усложнить поиск подходящих библиотек или инструментов для ваших конкретных нужд.

Экосистема и библиотеки Go

Go имеет более развитую экосистему по сравнению с Rust, с большой стандартной библиотекой и множеством библиотек сторонних разработчиков, доступных через систему управления пакетами Go Modules. Экосистема Go включает библиотеки для веб-разработки, баз данных, сетей и многого другого, что позволяет легко находить и использовать существующие решения для большинства задач разработки бэкенда.

Широкая поддержка библиотек и развитая экосистема Go могут помочь разработчикам быстро создавать и внедрять приложения, сокращая необходимость в разработке на заказ или обходных путях. Это может стать значительным преимуществом для команд, которые ценят быструю разработку и простоту использования.

Сравнение экосистемы и библиотек

При сравнении Rust и Go с точки зрения экосистемы и библиотек, Go имеет явное преимущество благодаря своей более развитой экосистеме и большой стандартной библиотеке. Широкая поддержка библиотек Go может помочь разработчикам быстро создавать и развертывать приложения, что делает его привлекательным выбором для проектов по разработке бэкенда, для которых приоритетны скорость и простота использования.

Экосистема Rust все еще развивается и имеет множество сторонних библиотек, но для определенных случаев использования может не хватать опций или зрелых библиотек. Этот компромисс может быть приемлемым для некоторых разработчиков, особенно для тех, кто работает над проектами, требующими уникальных возможностей и преимуществ производительности Rust.

Кривая обучения и сообщество

Кривая обучения и поддержка языка программирования сообществом могут значительно повлиять на легкость внедрения и успех проекта. Язык с крутой кривой обучения может потребовать больше времени и усилий для приобретения навыков, в то время как язык с сильным сообществом и широкими ресурсами может помочь разработчикам быстро преодолеть трудности и освоить лучшие практики.

Кривая обучения и сообщество Rust

Rust часто считается более сложным языком по сравнению с Go из-за уникальной системы владения и более сложных функций языка. Тем не менее, сообщество Rust известно своей дружелюбностью и поддержкой, а также обилием ресурсов, помогающих разработчикам изучать язык и преодолевать трудности.

Сообщество Rust подготовило обширную документацию, учебники и обучающие ресурсы, такие как официальная книга по Rust, Rust by Example и курс Rustlings. Кроме того, сообщество Rust активно работает на форумах, в чатах и социальных сетях, являясь ценным источником поддержки и знаний для разработчиков любого уровня подготовки.

Кривая обучения Go и сообщество

Считается, что Go имеет меньшую кривую обучения, чем Rust, благодаря своей простоте и минимальному синтаксису. Благодаря прямолинейному подходу к программированию на Go разработчики могут быстро освоить язык и начать создавать приложения с минимальными усилиями.

Сообщество Go также довольно большое и активное, с множеством ресурсов, доступных для изучения языка и преодоления трудностей. Официальный сайт Go предоставляет обширную документацию и учебники, а сообщество Go активно работает на форумах, в чатах и социальных сетях, обеспечивая поддержку и знания для разработчиков любого уровня подготовки.

Сравнение кривой обучения и сообщества

Если сравнивать Rust и Go с точки зрения кривой обучения и поддержки сообщества, то Go обычно считается более легким в изучении благодаря своей простоте и минимальному синтаксису. Это может сделать его привлекательным выбором для разработчиков, которые ценят продуктивность и простоту использования или являются новичками в системном программировании.

С другой стороны, Rust имеет более сложную кривую обучения из-за уникальной системы владения и более сложных функций языка. Однако сильное сообщество и богатые учебные ресурсы Rust могут помочь разработчикам преодолеть эти трудности и овладеть языком. Это может быть достойным компромиссом для разработчиков, которых привлекает безопасность, производительность и гибкость Rust.

Rust и Go имеют свои собственные кривые обучения и поддержку сообщества, удовлетворяя различные потребности и предпочтения разработчиков. Rust может больше подойти тем, кто готов потратить время и усилия на освоение его уникальных возможностей и воспользоваться преимуществами его безопасности и производительности. Go, вероятно, больше подходит тем, кто при изучении нового языка для разработки бэкенда отдает предпочтение простоте, быстрой разработке и легкости в использовании.

Примеры использования и принятие в отрасли

Примеры использования и отраслевое применение языка программирования могут дать ценную информацию о его пригодности для разработки бэкенда. Rust и Go были приняты различными отраслями и компаниями, каждая из которых имеет свои уникальные требования и сценарии использования.

Примеры использования Rust и его применение в промышленности

Rust находит все большее применение в таких отраслях, как веб-разработка, системное программирование, встраиваемые системы и разработка игр. Такие компании, как Mozilla, Dropbox и Cloudflare, используют Rust для своей критической инфраструктуры и высокопроизводительных систем. Ориентация Rust на безопасность, производительность и параллелизм делает его хорошо подходящим для таких требовательных приложений.

Кроме того, поддержка WebAssembly в Rust позволила ему стать популярным выбором для создания высокопроизводительных веб-приложений, запускаемых в браузере, что еще больше расширило сферу его использования и распространение в промышленности.

Примеры использования Go и внедрение в промышленности

Go получил широкое распространение в таких отраслях, как веб-разработка, облачные вычисления и распределенные системы. Такие компании, как Google, Uber и Kubernetes, выбрали Go для своих масштабных внутренних систем и инфраструктуры, оценив его простоту, удобство использования и масштабируемость. Легкая модель параллелизма Go и ориентация на производительность разработчиков делают его привлекательным выбором для таких типов приложений. Go также является популярным выбором для создания API, микросервисов и бессерверных функций благодаря сильной стандартной библиотеке и поддержке современных методов разработки.

Сравнение примеров использования и внедрения в промышленности

Если сравнивать Rust и Go с точки зрения примеров использования и внедрения в отрасли, то оба языка нашли успех в различных приложениях и отраслях. Rust хорошо подходит для высокопроизводительных, критичных к безопасности систем, в то время как Go часто выбирают для крупномасштабных внутренних систем и инфраструктуры, для которых приоритетны простота и удобство использования.

В конечном счете, выбор между Rust и Go для разработки будет зависеть от вашего конкретного случая использования, требований к производительности и приоритетов разработки. Оба языка доказали свою ценность в различных сценариях и могут быть успешным выбором для проектов.

Статья Rust vs Go

Рассмотрим пример main.go:

package main
import "fmt"
 
func main() {
    fmt.Println("Hello Go!")
}
package main // определили пакет main

import "fmt" // импортировали пакет fmt

// функция для входа в программу
func main() {
	var name string // объявили переменную name типа string
	fmt.Scan(&name) // считали переменную name с потока ввода

	// функция Print() печатает сообщение в консоль
	fmt.Print("Hello, ", name)
}

В Go есть два способа запуска программ:

  • С помощью команды go run <имя_файла>.go . Она автоматически компилирует исполняемый файл, запускает его и сразу же удаляет. Этот способ используется в тех случаях, когда нужно разово запустить небольшой кусок кода и быстро получить результат.
  • С помощью команды go build <имя_файла>.go . Она также занимается компиляцией и создает в директории исполняемый файл, способный запускаться на разных ОС. Особенностью этой команды является то, что она обеспечивает кроссплатформенность, так как позволяет контролировать, для какой ОС и архитектуры создается бинарный файл.

Рассмотрим второй пример main.rs:

fn main() {
    println!("Привет, мир!");
}
$ rustc main.rs
$ ./main
Hello, world!
fn main() {
    let mut x = 5;
    println!("The value of x is: {x}");
    x = 6;
    println!("The value of x is: {x}");
}
Справочник по Си
Руководство по Си