Перейти к основному содержимому

Типы данных в С

Разработчику Архитектору

Дальше: Справочник C · Работа с данными в C


Типы данных в С

Как читать эту статью

Примитивы → struct / union → указатели и строки; интерактивы в середине статьи. Нужны архитектура; дальше — управление.


Модель типов в С

В С тип задаёт размер в памяти и допустимые операции; ошибки с указателями и строками с нулём — главный источник уязвимостей.

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

Примитивные типы данных в С — это int, char, float, double и void. Каждый из них играет свою роль в организации данных и логики программы.


Поведение программы — defined, unspecified, undefined

Стандарт C различает три уровня предсказуемости:

  • Определённое поведение (defined) — результат описан в стандарте (например, 2 + 2 == 4, корректное разыменование валидного указателя).
  • Неуточнённое (unspecified) — допустимо несколько вариантов, но программа не "ломается" (например, порядок вычисления аргументов функции).
  • Неопределённое (undefined behavior, UB) — стандарт не накладывает ограничений; программа может завершиться сбоем, выдать мусор или "казаться рабочей". Примеры — выход за границы массива, разыменование NULL, двойной free, i = i++ + ++i.

На практике UB — главная причина уязвимостей и "призрачных" багов в C. Компилятор с -Wall -Wextra и санитайзеры (-fsanitize=address,undefined) помогают отловить часть таких мест ещё на этапе разработки.

Пример неопределённого поведения — выход за границы массива:

int arr[3] = {1, 2, 3};
int x = arr[3]; /* индексы 0..2, arr[3] — UB */
(void)x;

Разбор:

  • Массив из 3 элементов допускает индексы 0, 1, 2.
  • Обращение к arr[3] читает память за пределами объекта — это UB, даже если "иногда работает".
  • Компилятор может оптимизировать код, исходя из предположения, что границы не нарушаются.
  • AddressSanitizer (-fsanitize=address) часто ловит такие обращения во время выполнения.
  • Правильный доступ: цикл for (int i = 0; i < 3; i++).

Переносимые целые типы фиксированной ширины:

#include <stdint.h>
#include <stdio.h>

int main(void) {
uint32_t packet_id = 0x00AABBCC;
printf("id = 0x%08X, size = %zu\n", packet_id, sizeof packet_id);
return 0;
}

Разбор:

  • #include <stdint.h> подключает типы вроде uint32_t, int64_t с гарантированной шириной.
  • uint32_t всегда 32 бита без знака — удобно для сетевых пакетов и бинарных форматов.
  • 0x00AABBCC — шестнадцатеричный литерал; %08X печатает 8 hex-цифр с ведущими нулями.
  • sizeof packet_id показывает размер типа в байтах на текущей платформе (для uint32_t обычно 4).
  • Для "обычного" прикладного кода часто хватает int, но для протоколов лучше stdint.h.

Play ITЗагрузка интерактивного демо…


Соглашения об именах переменных

Имя переменной в С — идентификатор: буквы, цифры и _, без пробелов. Имя не может начинаться с цифры и не должно совпадать с ключевым словом (int, return, while и т.д.).

ПравилоПример
Осмысленное имяscore, buffer_size
Регистр различаетсяcount и Count — разные переменные
Цифры после первой буквыitem1, port2
Подчёркиваниеis_ok, _reserved (ведущий _ — осторожно в системном коде)
Недопустимо2bad, my-var, a+b, ключевое слово volatile

В учебных примерах избегайте "магических" имён вроде x1 там, где подойдут rows или temperature.


Целочисленный тип — int

Play ITЗагрузка интерактивного демо…

Тип int предназначен для хранения целых чисел — значений без дробной части. Это один из самых часто используемых типов в программах на С. Размер и диапазон int не фиксированы стандартом — они зависят от платформы и ABI. На типичных 32- и 64-битных системах (ILP32, LP64) int часто занимает 4 байта, но единственный надёжный способ узнать размер на целевой машине — оператор sizeof(int). Для переносимых протоколов и форматов данных используйте типы из <stdint.h> (int32_t, uint64_t и т.д.).

Проверка размеров типов на вашей машине:

#include <stdio.h>

int main(void) {
printf("sizeof(char) = %zu\n", sizeof(char));
printf("sizeof(int) = %zu\n", sizeof(int));
printf("sizeof(long) = %zu\n", sizeof(long));
printf("sizeof(void*) = %zu\n", sizeof(void*));
return 0;
}

Разбор:

  • sizeof вычисляется на этапе компиляции и возвращает размер типа/объекта в байтах.
  • %zu — корректный спецификатор для size_t, который возвращает sizeof.
  • sizeof(char) почти всегда равен 1, но char может быть signed или unsigned — это отдельный вопрос.
  • sizeof(void*) показывает разрядность адреса (часто 4 на 32-bit и 8 на 64-bit).
  • Такой сниппет полезно запускать при переносе кода между платформами.

Переменная типа int объявляется с помощью ключевого слова int, за которым следует имя переменной:

int age = 25;

Разбор:

  • int объявляет целочисленную переменную фиксированного типа.
  • age — идентификатор переменной, доступный в текущей области видимости.
  • = 25 выполняет инициализацию при создании, поэтому у переменной сразу валидное значение.
  • Такой шаблон используется для счётчиков, индексов и целочисленных параметров в логике программы.

Этот код создаёт переменную с именем age, присваивает ей значение 25 и резервирует в памяти место, достаточное для хранения одного целого числа.


Простой пример

#include <stdio.h>

int main(void) {
int score = 100;
printf("Ваш счёт: %d\n", score);
return 0;
}

Разбор:

  • Заголовок stdio.h подключает объявление printf, нужное для корректной компиляции.
  • main(void) — точка входа, где стартует выполнение пользовательского кода.
  • int score = 100; создаёт локальную переменную на стеке и инициализирует её.
  • printf("Ваш счёт: %d\n", score); подставляет значение score в формат %d.
  • return 0; сообщает ОС об успешном завершении программы.

Программа выводит:

Ваш счёт: 100

Здесь используется спецификатор формата %d, предназначенный для вывода целых чисел.

Форматы вывода printf

Второй и следующие аргументы printf подставляются в строку формата по спецификаторам после %. Частые варианты для учебных программ:

СпецификаторТип аргументаНазначение
%dintцелое в десятичном виде
%cint (код символа)один символ
%fdoubleвещественное (литералы float продвигаются)
%schar *C-строка до \0
%pvoid *адрес (печатать как (void *)ptr)
%zusize_tразмер, результат sizeof
%%символ % в выводе

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

Код ITЗагрузка примера кода…

Разбор:

  • %7d резервирует минимум 7 позиций под целое; лишние заполняются пробелами слева.
  • %07d заполняет слева нулями (удобно для дат и кодов).
  • %.10f и %20.3f управляют числом знаков после запятой и шириной поля; - в %-20.3f выравнивает влево.
  • %% в формате печатает один символ %, а не начинает новый спецификатор.

Полный список — в справочнике ./8.md и документации printf для вашего стандарта C.


Ввод с клавиатуры — scanf

scanf читает данные из stdin по тому же принципу форматных строк, что и printf, но для записи в переменные нужны адреса:

int age;
if (scanf("%d", &age) != 1) {
fprintf(stderr, "Ожидалось целое число\n");
return 1;
}

Разбор:

  • %d сопоставляет целое; без & компилятор примет вызов, но запись пойдёт "в мусор" — типичная ошибка новичка.
  • Возвращаемое значение — число успешно сопоставленных полей; для одного %d ожидается 1.
  • После неудачного ввода в буфере stdin могут остаться символы; в циклах валидации их очищают (см. управляющие конструкции).

Для строк не используйте scanf("%s", buf) без ограничения длины — возможно переполнение буфера. Безопаснее scanf("%99s", buf) при буфере char buf[100] или построчный ввод через fgets (файловый ввод-вывод, первая программа).


Сложный пример

#include <stdio.h>

int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n - 1);
}

int main(void) {
int number = 7;
int result = factorial(number);
printf("Факториал числа %d равен %d\n", number, result);
return 0;
}

Разбор:

  • factorial(int n) — рекурсивная функция, вычисляющая факториал целого числа.
  • База рекурсии if (n <= 1) return 1; останавливает дальнейшие вызовы и предотвращает бесконечную рекурсию.
  • Выражение n * factorial(n - 1) строит цепочку вызовов до базового случая и затем сворачивает результат обратно.
  • В main функция вызывается для number = 7, результат сохраняется в result и выводится через printf.
  • Для больших n тип int переполняется, поэтому в практическом коде часто берут long long или библиотеки big integer.

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


Квалификаторы целых — short, long, unsigned

К базовому int добавляют модификаторы размера и знака:

ЗаписьСмысл
short / short intобычно 16 бит
long / long intне короче int
long long (C99+)расширенное целое
unsignedтолько неотрицательные значения, удвоенный верхний диапазон
unsigned long, unsigned shortкомбинации

Пример:

unsigned short port = 8080;
long big_counter = 1000000L; /* суффикс L — литерал типа long */

Размеры на платформе проверяйте через sizeof; для протоколов и файлов — <stdint.h> (uint16_t, uint32_t).

Глобальные и локальные переменные. Локальные объявляются внутри блока (чаще всего в main или функции) и живут на стеке. Глобальные и static с областью файла — в сегментах data/BSS и существуют всё время работы процесса. Глобальные усложняют тесты и многопоточность; в новых проектах их сводят к минимуму.

Ключевое слово register — подсказка компилятору держать значение в регистре процессора; современные оптимизаторы часто игнорируют её. В учебном коде достаточно знать, что оно есть в стандарте C89.


Символьный тип — char

Тип char предназначен для хранения одного символа. Внутри компьютера символы представлены числовыми кодами согласно таблице ASCII или её расширениям. Размер типа char всегда равен 1 байту, что позволяет ему хранить значения от -128 до 127 (если char знаковый) или от 0 до 255 (если беззнаковый). По умолчанию поведение зависит от компилятора, но чаще всего char рассматривается как знаковый.

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

char letter = 'A';

Разбор:

  • char хранит один байт, который интерпретируется как символьный код.
  • 'A' — символьный литерал в одинарных кавычках, не строка.
  • Внутри памяти это число из таблицы кодировки (обычно ASCII/UTF-8-совместимые значения для латиницы).
  • Такой тип удобен для посимвольной обработки текста и буферов.

Символ заключается в одинарные кавычки, чтобы отличать его от строки.


Простой пример

#include <stdio.h>

int main(void) {
char initial = 'T';
printf("Ваша инициал: %c\n", initial);
return 0;
}

Разбор:

  • Программа создаёт переменную initial типа char и задаёт ей символ 'T'.
  • %c в printf интерпретирует аргумент как символ, а не как число.
  • Символ выводится как человекочитаемый знак в консоль.
  • main возвращает 0, подтверждая нормальное завершение.

Вывод:

Ваша инициал: T

Используется спецификатор %c для вывода одного символа.


Сложный пример

Код ITЗагрузка примера кода…

Разбор:

  • char buffer[10]; выделяет буфер на 10 символов в стеке.
  • Символы записываются вручную по индексам, формируя слово Hello.
  • buffer[5] = '\0'; ставит терминатор строки; без него %s в printf продолжит чтение памяти дальше буфера.
  • printf("Сообщение: %s\n", buffer); печатает массив как C-строку, ориентируясь на \0.
  • Это демонстрирует, что строка в C — это просто массив char с нулём в конце.

Хотя здесь используется массив символов, каждый элемент массива имеет тип char. Этот пример демонстрирует, как отдельные символы собираются в строку — важнейшую концепцию в С, где строки реализованы как последовательности значений типа char.


Вещественные типы — float и double

Для работы с числами, имеющими дробную часть, в С предусмотрены два типа: float и double. Оба представляют числа с плавающей запятой, но отличаются точностью и размером.

  • Тип float на большинстве платформ занимает 4 байта и даёт точность около 6–7 десятичных цифр (стандарт IEEE-754).
  • Тип double обычно 8 байт и даёт точность около 15–16 десятичных цифр. Точный размер и представление проверяйте через sizeof и документацию компилятора.

По умолчанию вещественные литералы в С имеют тип double. Чтобы явно указать float, к числу добавляют суффикс f.


Простой пример с float

#include <stdio.h>

int main(void) {
float price = 19.99f;
printf("Цена: %.2f рублей\n", price);
return 0;
}

Разбор:

  • float price = 19.99f; создаёт число с плавающей точкой одинарной точности.
  • Суффикс f важен: без него литерал имел бы тип double.
  • %.2f форматирует вывод с двумя знаками после запятой, что удобно для денежных примеров.
  • Этот тип компактнее double, но даёт меньшую точность вычислений.

Вывод:

Цена: 19.99 рублей

Спецификатор %.2f ограничивает вывод двумя знаками после запятой.


Простой пример с double

#include <stdio.h>

int main(void) {
double pi = 3.141592653589793;
printf("Число Пи: %.10f\n", pi);
return 0;
}

Разбор:

  • double pi хранит число с большей точностью, чем float.
  • Формат %.10f выводит 10 знаков после запятой, показывая запас точности типа double.
  • При выводе значение округляется согласно правилам форматирования printf.
  • Такой тип применяют там, где накопление ошибок округления критично.

Вывод:

Число Пи: 3.1415926536

Сложный пример — сравнение точности

#include <stdio.h>

int main(void) {
float a = 1.0f / 3.0f;
double b = 1.0 / 3.0;

printf("float: %.10f\n", a);
printf("double: %.10f\n", b);
return 0;
}

Разбор:

  • a вычисляется как float, а b как double, поэтому точность результата разная уже на этапе деления.
  • Оба значения печатаются с одинаковым форматом %.10f, что наглядно показывает различие в представлении.
  • float быстрее и меньше по памяти, но быстрее теряет точность при длинных вычислениях.
  • Пример подчёркивает практическую причину выбирать тип под задачу, а не "по привычке".

Вывод может быть таким:

float: 0.3333333433
double: 0.3333333333

Разница в точности становится заметной при высоких требованиях к вычислениям — например, в научных расчётах или графике.


Специальный тип — void

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

  • Функция, возвращающая void, ничего не возвращает вызывающему коду.
  • Указатель типа void* может указывать на данные любого типа, что делает его универсальным инструментом для работы с памятью.

Простой пример — функция без возврата

#include <stdio.h>

void greet() {
printf("Привет, мир!\n");
}

int main(void) {
greet();
return 0;
}

Разбор:

  • void greet() объявляет функцию без возвращаемого значения.
  • Внутри она выполняет побочный эффект: выводит строку через printf.
  • В main функция вызывается как обычная процедура, а её результат не используется.
  • void в возвращаемом типе выражает намерение: функция делает действие, а не вычисляет значение.

Функция greet объявлена с возвращаемым типом void, потому что её задача — только вывести сообщение, а не вернуть результат.


Сложный пример — универсальный указатель

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

int main(void) {
int number = 42;
void* ptr = &number; // указатель на любые данные

// Чтобы прочитать значение, нужно привести тип
int* int_ptr = (int*)ptr;
printf("Значение через void*: %d\n", *int_ptr);

return 0;
}

Разбор:

  • void* ptr = &number; сохраняет адрес переменной в универсальном указателе без жёсткой привязки к типу.
  • Для чтения данных обязательно приведение к исходному типу: int* int_ptr = (int*)ptr;.
  • Разыменование *int_ptr получает реальное значение number.
  • Такой паттерн часто встречается в API стандартной библиотеки (malloc, qsort, memcpy), где нужен тип-независимый интерфейс.

Указатель void* часто используется в стандартной библиотеке С — например, в функциях malloc и memcpy, которые работают с блоками памяти независимо от их содержимого.


Производные типы данных

Если примитивные типы — это кирпичи, то производные типы — это здания, построенные из этих кирпичей. Они позволяют организовывать данные в более сложные и осмысленные формы, адаптированные под конкретные задачи программы. В С к производным типам относятся — массивы, указатели, структуры (struct) и объединения (union). Все они создаются на основе примитивных типов, но обладают собственными правилами и возможностями.


Интерактив ниже иллюстрирует идеи на JavaScript/Python/C#; синтаксис массивов и списков в С — в примерах кода выше и ниже.

Play ITЗагрузка интерактивного демо…


Константы — const, enum и #define

МеханизмКогда удобен
constименованное значение с типом, проверяется компилятором
enumнабор связанных целочисленных констант
#defineмакрос препроцессора, подстановка текста до компиляции
const double PI = 3.1415926536;

enum Weekday { MON = 1, TUE, WED, THU, FRI, SAT, SUN };

#define MAX_USERS 128
#define SQUARE(x) ((x) * (x))

Разбор:

  • const запрещает присваивание переменной; для массивов и указателей читайте правила квалификаторов в стандарте.
  • В enum перечислители получают целые значения; TUE после MON = 1 станет 2.
  • #define MAX_USERS не создаёт переменную — препроцессор подставит 128 везде, где встретит имя.
  • Макрос SQUARE(x) оборачивают в скобки, иначе SQUARE(a+b) раскроется неверно; для выражений предпочтительны static inline функции (C99+) или обычные функции.

Отладочные макросы часто оборачивают в #ifdef DEBUG (препроцессор). Подробнее о выборе стиля констант — в идиомах.


Массивы

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

Объявление массива включает тип элементов, имя массива и количество элементов в квадратных скобках:

int numbers[5]; // массив из пяти целых чисел

Разбор:

  • Объявляется статический по размеру массив из 5 элементов типа int.
  • Все элементы расположены подряд в памяти, что обеспечивает быстрый индексный доступ.
  • Индексы допустимы от 0 до 4; выход за границы — неопределённое поведение.
  • Размер фиксирован на этапе компиляции (если не использовать динамическую память).

После объявления можно присваивать значения отдельным элементам:

numbers[0] = 10;
numbers[1] = 20;
// ... и так далее

Разбор:

  • Доступ к элементам массива выполняется через индекс в квадратных скобках.
  • numbers[0] и numbers[1] изменяют первые два элемента массива.
  • Каждый элемент имеет тот же тип int, что и у всего массива.
  • Комментарий показывает, что запись продолжается аналогично для остальных позиций.

Простой пример

#include <stdio.h>

int main(void) {
int days[7] = {1, 2, 3, 4, 5, 6, 7};
printf("Первый день недели: %d\n", days[0]);
return 0;
}

Разбор:

  • Массив days инициализируется сразу семью значениями при объявлении.
  • days[0] обращается к первому элементу, потому что индексация в C начинается с нуля.
  • printf с %d выводит значение целого элемента массива.
  • Пример закрепляет базовый принцип адресной модели: имя массива + индекс.

Программа выводит:

Первый день недели: 1

Инициализация массива при объявлении позволяет задать начальные значения всем или части его элементов.


Сложный пример — работа с двумерным массивом

Код ITЗагрузка примера кода…

Разбор:

  • Объявляется двумерный массив matrix[3][3], то есть таблица 3x3 целых чисел.
  • Вложенные циклы for проходят по строкам (i) и столбцам (j) матрицы.
  • matrix[i][j] получает конкретный элемент на пересечении строки и столбца.
  • printf("%d ", ...) печатает строку матрицы в одну линию, а printf("\n") переносит вывод после каждой строки.
  • Такой подход является базовым шаблоном для обработки табличных данных в C.

Вывод:

1 2 3
4 5 6
7 8 9

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

Объявление int i в заголовке for (как в примере выше) допустимо начиная с C99. В режиме C89 переменную цикла нужно объявить в начале блока функции.


Указатели

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

Тип указателя определяет, на данные какого типа он может указывать. Например, int* — это указатель на целое число, char* — на символ.

Объявление указателя:

int value = 42;
int* ptr = &value; // & — оператор взятия адреса

Разбор:

  • value хранит само значение, а ptr хранит адрес, где это значение лежит.
  • Тип int* гарантирует, что разыменование будет читать данные как int.
  • Оператор & возвращает адрес переменной в памяти.
  • Этот шаблон связывает обычную переменную и указатель на неё для дальнейшей передачи в функции.

Чтобы получить значение по адресу, используется оператор разыменования *:

int copy = *ptr; // copy получит значение 42

Разбор:

  • *ptr разыменовывает указатель и получает значение по сохранённому адресу.
  • Результат присваивается в copy, то есть создаётся отдельная копия числа.
  • После этого изменение copy не изменит value, так как это уже разные ячейки памяти.
  • Такой доступ лежит в основе работы с динамической памятью и параметрами "по указателю".

Простой пример

#include <stdio.h>

int main(void) {
int number = 100;
int* p = &number;

printf("Значение: %d\n", *p);
printf("Адрес: %p\n", (void*)p);
return 0;
}

Разбор:

  • p получает адрес переменной number, поэтому через *p читается тот же объект данных.
  • Первая строка вывода печатает значение переменной через разыменование указателя.
  • Вторая строка с %p печатает сам адрес; приведение к (void*) соответствует требованиям стандарта для %p.
  • Пример показывает разницу между "значением по адресу" и "самим адресом".

Программа выводит значение переменной и её адрес в памяти.


Сложный пример — изменение переменной через указатель

#include <stdio.h>

void increment(int* x) {
(*x)++;
}

int main(void) {
int counter = 5;
printf("До: %d\n", counter);
increment(&counter);
printf("После: %d\n", counter);
return 0;
}

Разбор:

  • increment(int* x) принимает адрес переменной, а не её копию.
  • (*x)++ увеличивает значение именно по переданному адресу.
  • В main передаётся &counter, поэтому функция меняет оригинальную переменную counter.
  • Разница "До/После" в выводе демонстрирует эффект передачи по указателю в C.

Вывод:

До: 5
После: 6

Функция increment принимает указатель на целое число и увеличивает значение по этому адресу. Без указателей такая модификация была бы невозможна, потому что С передаёт аргументы в функции по значению.


Структуры (struct)

Play ITЗагрузка интерактивного демо…

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

Объявление структуры начинается с ключевого слова struct, за которым следует имя структуры и список полей в фигурных скобках:

struct Person {
char name[50];
int age;
float height;
};

Разбор:

  • struct Person задаёт пользовательский составной тип с полями разных типов.
  • name[50] — фиксированный буфер под строку имени, age и height — числовые характеристики.
  • Такой тип собирает связанные данные в единую сущность, упрощая интерфейсы функций.
  • Определение структуры не создаёт переменную, а только описывает формат данных.

После объявления можно создавать переменные этого типа:

#include <string.h>

struct Person alice;
snprintf(alice.name, sizeof alice.name, "%s", "Alice");
alice.age = 30;
alice.height = 165.5f;

Разбор:

  • struct Person alice; создаёт экземпляр структуры Person.
  • snprintf безопасно копирует строку в alice.name с учётом размера буфера.
  • sizeof alice.name автоматически даёт длину массива и защищает от переполнения.
  • Поля age и height инициализируются напрямую через оператор ..
  • Такой стиль предпочтительнее небезопасных функций копирования строк без ограничения длины.

Простой пример

Код ITЗагрузка примера кода…

Разбор:

  • Структура Book объединяет название и число страниц в один логический объект.
  • snprintf заполняет поле title безопасно для фиксированного массива символов.
  • b.pages = 1225; записывает числовое значение в соответствующее поле структуры.
  • printf читает поля через . и выводит их в одной форматированной строке.

snprintf ограничивает запись размером буфера и всегда завершает строку \0 (если размер буфера больше нуля). Это безопаснее, чем strcpy, который не знает длины приёмника.

Вывод:

Book: War and Peace, pages: 1225

Сложный пример — массив структур и функция обработки

Код ITЗагрузка примера кода…

Разбор:

  • struct Student group[] создаёт массив структур, где каждый элемент хранит имя и оценку.
  • size вычисляется универсально через sizeof, без "магических чисел".
  • Функция print_top_students проходит по массиву и фильтрует студентов по условию grade >= 90.
  • Доступ к полям через students[i].name и students[i].grade показывает работу с коллекциями структур.
  • Это практический шаблон: данные + функция обработки + критерий фильтрации.

Вывод:

Отличник: Мария (оценка: 95)
Отличник: Анна (оценка: 92)

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


Объединения (union)

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

Объявляется объединение с помощью ключевого слова union:

union Payload {
int i;
float f;
char str[20];
};

Разбор:

  • union объединяет разные представления данных в одной области памяти.
  • Все поля начинаются с одного и того же адреса, поэтому запись в одно поле меняет "сырые биты" для остальных.
  • Размер объединения определяется максимальным полем (str[20] в данном примере).
  • Такой тип подходит для "вариантных" данных и компактных представлений.

Если записать в str, то предыдущие значения i и f будут утеряны, потому что вся память переиспользуется.


Простой пример

Код ITЗагрузка примера кода…

Разбор:

  • Сначала в v.integer записывается целое значение и корректно выводится как %d.
  • Затем в ту же память записывается v.real, то есть старое целое представление затирается.
  • Комментарий в коде подчёркивает ключевой принцип union: одновременно валидно обычно только последнее записанное поле.
  • Пример демонстрирует экономию памяти ценой более строгой дисциплины работы с типами.

Вывод:

Целое: 42
Вещественное: 3.14

Сложный пример — интерпретация байтов

Код ITЗагрузка примера кода…

Разбор:

  • В view.num записывается 32-битное число в шестнадцатеричном виде.
  • Цикл печатает байты этого числа через массив view.bytes, показывая физическое представление в памяти.
  • Формат %02X выводит каждый байт как двухсимвольное HEX-значение с ведущим нулём.
  • На little-endian порядок байтов "перевёрнут" относительно записи литерала, что и иллюстрирует пример.
  • Это полезно для понимания сериализации, протоколов и низкоуровневой отладки.

На машине с архитектурой little-endian вывод будет:

Байты (младший к старшему): 78 56 34 12

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


Строки в С — массивы символов

В языке С нет встроенного строкового типа. Вместо этого строки представляются как массивы символов, завершающиеся специальным нулевым символом \0 (null terminator). Этот символ сигнализирует о конце строки и обязателен для корректной работы стандартных функций, таких как printf, strlen, strncpy.

Массив и указатель: в выражениях имя массива часто преобразуется в указатель на первый элемент, но sizeof(array) по-прежнему даёт размер всего массива в байтах, а sizeof(pointer) — только размер указателя. Путаница между char s[] и char *s — частый источник ошибок при передаче строк в функции.

Например:

char message[] = "Привет";

Разбор:

  • message — массив символов, а не указатель; размер массива включает завершающий \0.
  • Литерал копируется в массив при инициализации, поэтому содержимое можно изменять поэлементно.
  • Нулевой терминатор нужен функциям работы со строками, чтобы понять, где строка заканчивается.
  • Для многобайтных UTF-8 символов "длина в байтах" и "длина в символах" могут различаться.

Компилятор автоматически добавляет \0 в конец, поэтому фактический размер массива — 7 байт (6 букв + 1 завершающий нуль).

Библиотека string.h — учебный маршрут

Подключение: #include <string.h>. Типичный порядок освоения:

ЗадачаФункцияБезопасная практика
длинаstrlen(s)s должен указывать на завершённую \0 строку
копияstrcpy / strncpyпредпочитайте snprintf(dest, sizeof dest, "%s", src) или strncpy + ручной \0
склейкаstrcat / strncatследите за размером приёмника
поискstrstr(haystack, needle)проверяйте NULL, если подстроки нет
сравнениеstrcmp, strncmp0 — равенство
преобразованиеatoi, strtolstrtol + проверка errno и хвоста строки
#include <string.h>
#include <stdio.h>

int main(void) {
char dest[32];
const char *src = "Hello";

snprintf(dest, sizeof dest, "%s", src);
printf("len=%zu [%s]\n", strlen(dest), dest);
return 0;
}

В старых учебниках встречаются голые strcpy/strcat — для своих программ закладывайте ограничение длины и проверку терминатора. Сигнатуры и краевые случаи — в справочнике.


Простой пример

#include <stdio.h>

int main(void) {
char greeting[] = "Здравствуйте!";
printf("%s\n", greeting);
return 0;
}

Разбор:

  • greeting[] инициализируется строковым литералом и содержит завершающий \0.
  • %s в printf выводит символы последовательно до первого нулевого байта.
  • Если убрать терминатор или повредить строку, %s может выйти за пределы буфера.
  • Пример показывает базовое безопасное чтение C-строки без ручной адресной арифметики.

Вывод:

Здравствуйте!

Сложный пример — ручное управление строкой

Код ITЗагрузка примера кода…

Разбор:

  • string_length принимает указатель на начало строки и вручную считает символы до \0.
  • Цикл while (*s != '\0') читает текущий символ через разыменование и двигает указатель s++.
  • len увеличивается на каждом шаге, поэтому в конце содержит длину строки в байтах до терминатора.
  • В main функция вызывается для массива text, который неявно передаётся как char*.
  • Это прямой аналог логики strlen, полезный для понимания внутренней механики строк в C.

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


Что дальше

ТемаСтатья
УправлениеУправляющие конструкции и операторы С
Функции, указателиФункции и указатели
Работа с даннымиСправочник по языку С
СправочникПримеры игр и системных утилит на С

Что попробовать

  1. Выведите sizeof для int, long, char на вашей платформе.
  2. Соберите struct и передайте указатель в функцию.
  3. Пройдите строку вручную до '\0' — пример из статьи.