Типы данных в С
Дальше: Справочник 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 подставляются в строку формата по спецификаторам после %. Частые варианты для учебных программ:
| Спецификатор | Тип аргумента | Назначение |
|---|---|---|
%d | int | целое в десятичном виде |
%c | int (код символа) | один символ |
%f | double | вещественное (литералы float продвигаются) |
%s | char * | C-строка до \0 |
%p | void * | адрес (печатать как (void *)ptr) |
%zu | size_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, strncmp | 0 — равенство |
| преобразование | atoi, strtol | strtol + проверка 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 вручную, демонстрируя, как строки обрабатываются через указатели на символы и завершающий нуль.
Что дальше
| Тема | Статья |
|---|---|
| Управление | Управляющие конструкции и операторы С |
| Функции, указатели | Функции и указатели |
| Работа с данными | Справочник по языку С |
| Справочник | Примеры игр и системных утилит на С |
Что попробовать
- Выведите
sizeofдляint,long,charна вашей платформе. - Соберите
structи передайте указатель в функцию. - Пройдите строку вручную до
'\0'— пример из статьи.