W5. Выражения в C, структуры (struct), битовые поля (bit-fields), выравнивание (alignment), объединения (union), перечисления (enum)

Автор

Eugene Zouev, Munir Makhmutov

Дата публикации

30 сентября 2025 г.

Quiz | Flashcards

1. Краткое содержание

1.1 Выражения (expressions) в C

Expression в C — это сочетание переменных, констант, операторов и вызовов функций, которое компилятор вычисляет до одного значения. Почти каждое выражение даёт значение; это отличает его от statement (оператора): оператор выполняет действие и не обязан иметь значение.

Выражения строятся из «кирпичиков», выстроенных по иерархии сложности.

1.1.1 Primary expressions (первичные выражения)

Самые базовые элементы выражений.

  • Identifiers (идентификаторы): имена переменных, функций или констант (например, myVariable, calculateSum, PI).
  • Literals (литералы): константы в коде — целые (123, 0xFE), с плавающей точкой (0.01E-2), строки ("string").
  • Parenthesized expressions (выражения в скобках): любое выражение в скобках, например (a + b), само является первичным; скобки задают порядок вычисления.
1.1.2 Secondary (Postfix) expressions (постфиксные выражения)

Строятся на первичных; оператор стоит после операнда.

  • Array subscripting (индексация массива): myArray[i + 1].
  • Function call (вызов функции): printf("Hello").
  • Member access (доступ к полю): для struct / union — точка . у объекта (student.id) или стрелка -> у указателя (studentPtr->id).
  • Postfix increment/decrement: x++, y-- — сначала используется текущее значение, затем переменная меняется на 1.
1.1.3 Unary expressions (унарные выражения)

Один операнд; оператор перед ним.

  • Prefix increment/decrement: ++x, --y.
  • Address-of (&): адрес переменной, &myVar.
  • Indirection (*): значение по адресу из указателя, *ptr.
  • Unary plus/minus: +10, -x.
  • Logical NOT (!): !isComplete.
  • Bitwise NOT (~): инверсия бит целого.
  • sizeof: размер типа или объекта в байтах, sizeof(int).
1.1.4 Binary expressions (бинарные выражения)

Два операнда с оператором между ними; на этом уровне обычно строятся самые «крупные» составные выражения из более простых частей.

  • Multiplicative: *, /, %.
  • Additive: +, -.
  • Bitwise shift: <<, >>.
  • Relational and equality: <, <=, >, >=, ==, !=.
  • Bitwise logical: &, |, ^.
  • Logical: &&, ||.
1.2 Структуры (struct)

Structure (struct) — пользовательский тип, объединяющий поля разных типов под одним именем, чтобы описывать сложные записи. Например, struct Person может хранить имя (строка), возраст (int), рост (float).

1.2.1 Объявление и использование

Структуру можно создать статически (на stack) или динамически (на heap).

  • Static declaration: struct Person p1; — объект на стеке.
  • Dynamic declaration: struct Person *p2 = (struct Person*)malloc(sizeof(struct Person)); — память на куче, возвращается указатель.
1.2.2 Доступ к полям
  • Dot operator (.): у значения struct, например p1.age = 30;.
  • Arrow operator (->): у указателя на struct, например p2->age = 30;.
1.2.3 Инициализация

Поля можно задать при объявлении. В C99 появились designated initializers (именованные инициализаторы): явно указываете поле по имени — код читается лучше.

  • Standard initialization: struct Point p = {10, 20};
  • Designated initializer: struct Point p = {.x = 10, .y = 20};
1.3 Битовые поля (bit-fields)

Bit-field — поле внутри struct с явной шириной в битах. Удобно экономить память, если диапазон значений мал.

Если нужны значения 0–7, достаточно 3 бит (\(2^3=8\)), тогда как обычный unsigned int занял бы 32 бита.

1.3.1 Синтаксис и применение

Синтаксис: type member_name : width;.

struct Flags {
    unsigned int isActive : 1;  // 1 bit: can be 0 or 1
    unsigned int level    : 3;  // 3 bits: can hold values 0-7
    unsigned int category : 4;  // 4 bits: can hold values 0-15
};

Типичные случаи:

  • упаковка данных под фиксированный размер (например, сетевые пакеты);
  • модель регистров оборудования, где биты имеют смысл;
  • несколько флагов в одном байте/слове.
1.3.2 Ограничения
  • нельзя взять адрес (&) у поля bit-field — оно может не совпадать с границей байта;
  • массивы битовых полей не разрешены;
  • точная раскладка в памяти может быть implementation-defined (зависит от компилятора).
1.4 Выравнивание (alignment)

Alignment — правила размещения типов в памяти. Процессоры часто читают память блоками (4 или 8 байт) и эффективнее работают, если объект размера N лежит по адресу, кратному N.

Компилятор вставляет между полями struct неиспользуемые байты — padding. Из-за этого sizeof(struct) не всегда равен сумме размеров полей.

Пример: char (1 байт) и затем int (4 байта) — после char часто добавляют 3 байта padding, чтобы int начался на границе 4 байт.

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

Union похож на struct, но все поля разделяют одну и ту же область памяти; размер union — по максимальному полю.

Одновременно «активно» одно поле в смысле интерпретации. Применение:

  • экономия памяти, если в каждый момент нужен только один из вариантов;
  • type punning — интерпретировать один и тот же блок памяти по-разному: например, записать 32-битное целое и затем прочитать отдельные байты через член char[4] того же union (в реальном коде учитывайте strict aliasing и переносимость).
1.6 Перечисления (enum)

Enumeration (enum) задаёт пользовательский целочисленный тип с именованными константами (enumerators), вместо «магических чисел» вроде 0, 1, 2 — читаемость и самодокументирование.

По умолчанию первый enumerator — 0, каждый следующий на 1 больше; можно задать значения явно.

enum TrafficLight {
    RED,         // Value is 0
    YELLOW,      // Value is 1
    GREEN        // Value is 2
};

enum Status {
    SUCCESS = 0,
    ERROR_FILE_NOT_FOUND = 101,
    ERROR_ACCESS_DENIED  = 102
};

2. Определения

  • Expression: сочетание переменных, констант, операторов и вызовов, вычисляемое в одно значение.
  • Structure (struct): составной тип, объединяющий поля, возможно разных типов.
  • Bit-field: поле struct с заданной шириной в битах для упаковки данных.
  • Alignment: требование к адресам объектов (часто кратность размеру типа).
  • Padding: служебные байты между полями struct для выравнивания.
  • Union (union): составной тип с общей памятью для всех полей; размер по максимальному полю.
  • Enumeration (enum): пользовательский целый тип с именованными константами (enumerators).

3. Примеры

3.1. Упаковать дату в 2 байта с помощью битовых полей (Лаба 5, Задание 1)

С помощью struct с битовыми полями упакуйте день, месяц и год рождения в 2 байта (все поля — числа). Числа задайте в коде, выведите значения полей и размер структуры.

Нажмите, чтобы увидеть решение
#include <stdio.h>

// 2 bytes = 16 bits. We need to allocate these 16 bits among day, month, and year.
// To represent day (1-31), we need 5 bits (2^5 = 32).
// To represent month (1-12), we need 4 bits (2^4 = 16).
// This leaves 16 - 5 - 4 = 7 bits for the year.
// 7 bits can represent numbers up to 2^7 - 1 = 127. We can store the year relative to a base year, e.g., 1980.
struct Date {
    // Allocate 5 bits for the day.
    unsigned int day : 5;
    // Allocate 4 bits for the month.
    unsigned int month : 4;
    // Allocate 7 bits for the year (e.g., year - 1980).
    unsigned int year : 7;
};

int main() {
    // Declare a variable of the Date structure type.
    struct Date birthDate;
    
    // Define the birth date components.
    int d = 22;
    int m = 10;
    int y = 1995;
    
    // Assign values to the bit fields.
    // The C compiler will handle packing the values into the specified bit allocations.
    birthDate.day = d;
    birthDate.month = m;
    birthDate.year = y - 1980; // Store the year as an offset

    // --- Print the stored values ---
    printf("--- Stored Date ---\n");
    // The values are retrieved from the bit fields.
    printf("Day: %u\n", birthDate.day);
    printf("Month: %u\n", birthDate.month);
    // Add the offset back to display the correct year.
    printf("Year: %u\n", birthDate.year + 1980);
    
    // --- Print the size of the structure ---
    // The sizeof operator returns the size of a variable or data type in bytes.
    // Due to memory alignment/padding, the compiler might make the struct larger than
    // the minimum 2 bytes. However, for a simple case like this, it's often 2.
    printf("\nSize of the Date structure: %zu bytes\n", sizeof(struct Date));

    return 0;
}
3.2. Разбор заголовка IPv4 через union и битовые поля (Лаба 5, Задание 2)

Используя union packet и struct с пятью соответствующими битовыми полями, напишите программу: запросить у пользователя целое, разобрать его и вывести поля заголовка IPv4 (Version, IHL, DSCP, ECN, Total Length).

Нажмите, чтобы увидеть решение
#include <stdio.h>

// A structure to represent the first 32 bits of an IPv4 header using bit fields.
// The order of fields might depend on the system's endianness (most are little-endian).
// We declare them from least significant to most significant bit position for portability.
struct IPv4_Header {
    unsigned int total_length : 16;
    unsigned int ecn : 2;
    unsigned int dscp : 6;
    unsigned int ihl : 4;
    unsigned int version : 4;
};

// A union to access the same 32 bits of memory as either a single unsigned integer
// or as the structured bit fields of the IPv4 header.
typedef union {
    unsigned int raw_value;
    struct IPv4_Header fields;
} Packet;

int main() {
    // Declare a union variable.
    Packet packet;

    // Prompt the user for a 32-bit integer value.
    printf("Enter a 32-bit integer value for the IPv4 header (e.g., 1162913537): ");
    scanf("%u", &packet.raw_value);

    // --- Print the parsed fields ---
    // By writing to `raw_value`, we have simultaneously set all the bit fields
    // within the `fields` structure, as they share the same memory.
    printf("\n--- Parsed IPv4 Header Fields ---\n");
    printf("Version: %u\n", packet.fields.version);
    printf("IHL (Header Length): %u (words)\n", packet.fields.ihl);
    printf("DSCP (Differentiated Services Code Point): %u\n", packet.fields.dscp);
    printf("ECN (Explicit Congestion Notification): %u\n", packet.fields.ecn);
    printf("Total Length: %u (bytes)\n", packet.fields.total_length);
    
    // Example: If input is 1162913537 (which is 0x45500501 in hex), the bits are:
    // 0100 0101 0101 0000 0000 0101 0000 0001
    // Version: 0100 -> 4
    // IHL: 0101 -> 5
    // DSCP: 010100 -> 20
    // ECN: 00 -> 0
    // Total Length: 0000 0101 0000 0001 -> 1281

    return 0;
}
3.3. День недели из целого через enum и switch (Лаба 5, Задание 3)

С помощью enum для дней недели реализуйте функцию со switch, переводящую номер дня в строку. Программа спрашивает число и печатает день (1 — Monday, …, 7 — Sunday).

Нажмите, чтобы увидеть решение
#include <stdio.h>

// An enumeration (enum) to define named integer constants for the days of the week.
// By default, MONDAY is 1, TUESDAY is 2, and so on.
typedef enum {
    MONDAY = 1,
    TUESDAY,
    WEDNESDAY,
    THURSDAY,
    FRIDAY,
    SATURDAY,
    SUNDAY
} Weekday;

// A function that takes a Weekday enum value and returns its string representation.
const char* getWeekdayName(Weekday day) {
    // The switch statement checks the value of the 'day' variable.
    switch (day) {
        case MONDAY:
            return "Monday";
        case TUESDAY:
            return "Tuesday";
        case WEDNESDAY:
            return "Wednesday";
        case THURSDAY:
            return "Thursday";
        case FRIDAY:
            return "Friday";
        case SATURDAY:
            return "Saturday";
        case SUNDAY:
            return "Sunday";
        default:
            // The default case handles any value that doesn't match the cases above.
            return "Invalid day";
    }
}

int main() {
    int dayNumber;

    // Prompt the user to enter a number.
    printf("Enter a number for the day of the week (1-7): ");
    scanf("%d", &dayNumber);

    // Check if the entered number is within the valid range.
    if (dayNumber >= MONDAY && dayNumber <= SUNDAY) {
        // Cast the integer to our Weekday enum type and call the function.
        const char* dayName = getWeekdayName((Weekday)dayNumber);
        printf("The corresponding weekday is: %s\n", dayName);
    } else {
        // If the number is out of range, print an error message.
        printf("Error: Please enter a number between 1 and 7.\n");
    }

    return 0;
}
3.4. Кулинарная книга: массив структур (Лаба 5, Задание 4)

Напишите программу с массивом структур для книги рецептов. У каждого рецепта — имя и от 2 до 10 ингредиентов (имя и количество). На выходе напечатайте всю книгу.

Нажмите, чтобы увидеть решение
#include <stdio.h>
#include <string.h>

// Define a maximum number of ingredients per recipe and a max number of recipes.
#define MAX_INGREDIENTS 10
#define MAX_RECIPES 5

// Structure for a single ingredient.
struct Ingredient {
    char name[50];
    char amount[20]; // e.g., "2 cups", "100g", "1 tsp"
};

// Structure for a single recipe.
struct Recipe {
    char name[100];
    int ingredient_count; // To know how many ingredients are actually used.
    struct Ingredient ingredients[MAX_INGREDIENTS];
};

// --- Function to print a single recipe ---
void printRecipe(struct Recipe r) {
    printf("--- Recipe: %s ---\n", r.name);
    printf("Ingredients:\n");
    for (int i = 0; i < r.ingredient_count; i++) {
        printf("- %s (%s)\n", r.ingredients[i].name, r.ingredients[i].amount);
    }
    printf("\n");
}


int main() {
    // Create an array of Recipe structures to act as our cookbook.
    struct Recipe cookbook[MAX_RECIPES];
    int recipe_count = 0; // Keep track of how many recipes we have.
    
    // --- Manually defining a few recipes for demonstration ---
    
    // Recipe 1: Scrambled Eggs
    strcpy(cookbook[recipe_count].name, "Scrambled Eggs");
    cookbook[recipe_count].ingredient_count = 3;
    strcpy(cookbook[recipe_count].ingredients[0].name, "Eggs");
    strcpy(cookbook[recipe_count].ingredients[0].amount, "2");
    strcpy(cookbook[recipe_count].ingredients[1].name, "Milk");
    strcpy(cookbook[recipe_count].ingredients[1].amount, "2 tbsp");
    strcpy(cookbook[recipe_count].ingredients[2].name, "Butter");
    strcpy(cookbook[recipe_count].ingredients[2].amount, "1 tsp");
    recipe_count++;
    
    // Recipe 2: Simple Pasta
    strcpy(cookbook[recipe_count].name, "Simple Pasta");
    cookbook[recipe_count].ingredient_count = 2;
    strcpy(cookbook[recipe_count].ingredients[0].name, "Pasta");
    strcpy(cookbook[recipe_count].ingredients[0].amount, "200g");
    strcpy(cookbook[recipe_count].ingredients[1].name, "Tomato Sauce");
    strcpy(cookbook[recipe_count].ingredients[1].amount, "1 cup");
    recipe_count++;
    
    // --- Print all the recipes in the cookbook ---
    printf("========= MY COOKBOOK =========\n\n");
    for (int i = 0; i < recipe_count; i++) {
        printRecipe(cookbook[i]);
    }

    return 0;
}
3.5. Сортировка структур с помощью enum (Лаба 5, Задание 5)

Программа с двумя enum: роль в Moodle (Student, TA, Professor) и степень degree (Secondary, Bachelor, Master, PhD), и структурой moodle_member с полями name, degree, position (роль). Запросите число участников, введите имена, роли и степени, отсортируйте сначала по роли, при равенстве — по степени, затем выведите результат.

Нажмите, чтобы увидеть решение
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Define the enumeration for roles. Lower integer value means lower priority.
typedef enum {
    STUDENT,
    TA,
    PROFESSOR
} MoodleRole;

// Define the enumeration for degrees. Lower integer value means lower priority.
typedef enum {
    SECONDARY,
    BACHELOR,
    MASTER,
    PHD
} Degree;

// Structure to hold a Moodle member's information.
typedef struct {
    char name[100];
    Degree degree;
    MoodleRole role;
} MoodleMember;

// --- Helper functions to convert enums to strings for printing ---
const char* getRoleName(MoodleRole r) {
    switch(r) {
        case STUDENT: return "Student";
        case TA: return "TA";
        case PROFESSOR: return "Professor";
        default: return "Unknown";
    }
}
const char* getDegreeName(Degree d) {
    switch(d) {
        case SECONDARY: return "Secondary";
        case BACHELOR: return "Bachelor";
        case MASTER: return "Master";
        case PHD: return "PhD";
        default: return "Unknown";
    }
}

// Comparison function for qsort().
// This function defines the sorting logic.
int compareMembers(const void *a, const void *b) {
    MoodleMember *memberA = (MoodleMember *)a;
    MoodleMember *memberB = (MoodleMember *)b;

    // First, compare by role.
    if (memberA->role < memberB->role) return -1;
    if (memberA->role > memberB->role) return 1;

    // If roles are equal, then compare by degree.
    if (memberA->degree < memberB->degree) return -1;
    if (memberA->degree > memberB->degree) return 1;

    // If both role and degree are equal, they are considered equal in sorting order.
    return 0;
}

int main() {
    int count;
    printf("Enter the number of Moodle members: ");
    scanf("%d", &count);
    
    // Dynamically allocate memory for the array of members.
    MoodleMember *members = malloc(count * sizeof(MoodleMember));
    
    // Get details for each member.
    for (int i = 0; i < count; i++) {
        printf("\n--- Member %d ---\n", i + 1);
        printf("Name: ");
        scanf("%s", members[i].name);
        printf("Role (0=Student, 1=TA, 2=Professor): ");
        scanf("%u", &members[i].role);
        printf("Degree (0=Secondary, 1=Bachelor, 2=Master, 3=PhD): ");
        scanf("%u", &members[i].degree);
    }
    
    // Sort the array using the standard library's qsort function.
    // qsort(array_to_sort, number_of_elements, size_of_each_element, comparison_function);
    qsort(members, count, sizeof(MoodleMember), compareMembers);

    // Print the sorted list of members.
    printf("\n--- Sorted Moodle Members ---\n");
    for (int i = 0; i < count; i++) {
        printf("Name: %-15s | Role: %-10s | Degree: %-10s\n", 
               members[i].name, 
               getRoleName(members[i].role), 
               getDegreeName(members[i].degree));
    }
    
    // Free the dynamically allocated memory.
    free(members);
    
    return 0;
}