Лекция 17: Структуры в языке С
"Я человек весьма терпимый, могу выслушать и чужие мнения."
— Ярослав Гашек
Введение
Лекция посвящена моделированию объектов реального мира с помощью композитных типов в языке Си. Рассмотрены механизмы объявления и использования структур, работу с указателями, динамическое выделение памяти, передача структур в функции, а также особенности перечислимых типов (enum) и объединений (union). Приведённые примеры кода помогают закрепить материал и понять практические аспекты работы с данными конструкциями языка.
Объявление структур
Основные положения:
- Структура – агрегатный тип, позволяющий объединить переменные разных типов под одним именем.
- Синтаксис объявления:
struct имя_структуры { тип1 поле1; тип2 поле2; // … };
- Если несколько полей имеют одинаковый тип, их можно объединить через запятую:
struct Point3D { int x, y, z; };
Пример объявления структуры:
struct point_t {
int x;
int y;
};
После объявления можно создавать переменные типа
struct point_t
и обращаться к полям через точку.
Инициализация и использование структур
Основные моменты:
- Создание переменной структуры сразу после объявления:
struct point_t { int x; int y; } A;
- Инициализация полей структуры происходит при создании переменной:
struct point_t A = {10, 20};
- Возможна именованная инициализация (с современным стандартом C):
typedef struct thing { int a; float b; const char *c; } thing_t; int main() { thing_t t = { .a = 10, .b = 1.0, .c = "example" }; // … }
Код для расчёта расстояния:
#include <stdio.h>
#include <math.h>
struct point_t {
int x;
int y;
};
int main() {
struct point_t A;
float distance;
A.x = 10;
A.y = 20;
distance = sqrt((float)(A.x * A.x + A.y * A.y));
printf("Distance = %.3f\n", distance);
return 0;
}
Пример демонстрирует создание структуры, инициализацию полей и вычисление расстояния от начала координат.
Указатели на структуры
Ключевые моменты:
- Для доступа к полям структуры по указателю используется оператор
->
:struct point_t A = {3, 4}; struct point_t *ptrA = &A; ptrA->x = 5; // то же, что (*ptrA).x = 5;
- Использование указателей позволяет эффективно работать с динамическими структурами и передавать их в функции.
Пример работы с указателем:
#include <stdio.h>
struct Point {
int x;
int y;
};
int main() {
struct Point A = {3, 4};
struct Point *ptrA = &A;
// Изменяем поля через указатель
ptrA->x = 5;
ptrA->y = 6;
printf("A: (%d, %d)\n", A.x, A.y);
return 0;
}
Работа с указателями позволяет получать прямой доступ к данным и изменять их в функции без копирования всей структуры.
Использование typedef для упрощения объявления типов
Суть:
- С помощью
typedef
можно создать псевдонимы для структур, чтобы не писать постоянно ключевое словоstruct
. - Пример:
typedef struct point_t { int x; int y; } Point; int main() { Point A, B = {10, 13}; // … }
Преимущества:
- Упрощает запись кода.
- Улучшает читаемость.
Пример демонстрирует, как использование
typedef
избавляет от необходимости писатьstruct
при создании переменной типа Point.
Динамические структуры данных
Основные аспекты:
- Выделение памяти под массив структур с помощью
malloc
. - При работе с динамическими структурами важно правильно освобождать память, так как поля структуры (особенно указатели) не освобождаются автоматически.
Пример выделения памяти и работы с массивом структур:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_SIZE 20
typedef struct User {
char *login;
char *password;
int id;
} User;
void jsonUser(User *user) {
printf("{id: %d, login: \"%s\", password: \"%s\"}\n",
user->id, user->login, user->password);
}
void freeUsersArray(User **users, unsigned size) {
for (unsigned i = 0; i < size; i++) {
free((*users)[i].login);
free((*users)[i].password);
}
free(*users);
}
int main() {
User *users = NULL;
unsigned size;
char buffer[128];
printf("Enter number of users: ");
scanf("%u", &size);
size = size <= MAX_SIZE ? size : MAX_SIZE;
users = (User*)malloc(size * sizeof(User));
for (unsigned i = 0; i < size; i++) {
printf("User #%u name: ", i);
scanf("%127s", buffer);
users[i].id = i;
users[i].login = (char*)malloc(strlen(buffer) + 1);
strcpy(users[i].login, buffer);
printf("Password: ");
scanf("%127s", buffer);
users[i].password = (char*)malloc(strlen(buffer) + 1);
strcpy(users[i].password, buffer);
}
for (unsigned i = 0; i < size; i++) {
jsonUser(&users[i]);
}
freeUsersArray(&users, size);
return 0;
}
Пример демонстрирует выделение памяти под динамический массив структур, заполнение полей и последующее освобождение памяти.
Передача структур в функции
Способы передачи:
- Передача по значению: копия всей структуры передается в функцию. Изменения внутри функции не влияют на оригинал.
- Передача по указателю: передается адрес структуры, что позволяет изменять содержимое непосредственно.
Пример передачи структуры по значению:
#include <stdio.h>
#include <conio.h>
struct struct_type {
int a, b;
char ch;
};
void fun(struct struct_type parm) {
printf("\n%d\n%d\n%c\n", parm.a, parm.b, parm.ch);
}
int main(void) {
struct struct_type arg;
arg.a = 1000;
arg.b = 9000;
arg.ch = 'A';
fun(arg);
printf("\nPress any key: ");
getch();
return 0;
}
Демонстрируется базовая передача структуры в функцию.
Копирование структур и работа с памятью
Ключевые моменты:
- Копирование «бит в бит»: можно использовать оператор присваивания или функцию
memcpy
. - При копировании массива структур важно учитывать размер и порядок элементов.
Пример копирования структуры:
#include <stdio.h>
#include <string.h>
struct Point {
int x;
int y;
};
int main() {
struct Point point = {10, 20};
struct Point apex;
// Поэлементное копирование
apex.x = point.x;
apex.y = point.y;
// Или с использованием memcpy
// memcpy(&apex, &point, sizeof(struct Point));
printf("a = %d, b = %d\n", apex.x, apex.y);
return 0;
}
Важно помнить, что приведение одной структуры к другой недопустимо, даже если их поля совпадают.
Битовые поля в структурах
Назначение:
- Позволяют упаковать данные более компактно, выделяя фиксированное число бит для хранения значения.
- Объявление битовых полей происходит с использованием двоеточия после имени поля.
Пример структуры с битовыми полями:
#include <stdio.h>
#define YEAR0 1980
struct date {
unsigned short day : 5; // 5 бит (значения 1..31)
unsigned short month : 4; // 4 бит (значения 1..12)
unsigned short year : 7; // 7 бит (смещение от YEAR0)
};
int main() {
struct date today;
today.day = 9;
today.month = 2;
today.year = 2022 - YEAR0; // Пример: 42
printf("\nToday: %u.%u.%u\n", today.day, today.month, today.year + YEAR0);
printf("Size of 'today': %lu bytes\n", sizeof(today));
return 0;
}
Битовые поля позволяют экономить память, однако требуется следить за диапазоном значений, которые могут быть сохранены в отведённом количестве бит.
Перечислимые типы (enum)
Основные сведения:
- Перечисления позволяют задать символические имена для набора целочисленных констант.
- Повышают читаемость кода и предотвращают использование "магических чисел".
Пример объявления и использования enum:
#include <stdio.h>
typedef enum {
MONDAY = 1,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY
} WEEK;
int main() {
int day;
printf("Введите номер дня недели: ");
scanf("%d", &day);
switch(day) {
case MONDAY:
printf("Понедельник\n");
break;
case TUESDAY:
printf("Вторник\n");
break;
case WEDNESDAY:
printf("Среда\n");
break;
case THURSDAY:
printf("Четверг\n");
break;
case FRIDAY:
printf("Пятница\n");
break;
case SATURDAY:
printf("Суббота\n");
break;
case SUNDAY:
printf("Воскресенье\n");
break;
default:
printf("Некорректный ввод\n");
break;
}
return 0;
}
Перечисления помогают задать понятные константы для дней недели и использовать их в конструкциях управления.
Объединения (union)
Что такое union:
- Объединение позволяет хранить разные типы данных в одном участке памяти, но только одно из полей активно в каждый момент времени.
- Размер объединения определяется размером самого большого поля.
Пример объединения:
#include <stdio.h>
#include <stdlib.h>
union u_tag {
int ival;
float fval;
char *sval;
};
int main() {
union u_tag u;
u.ival = 42;
printf("Integer: %d\n", u.ival);
u.fval = 3.14f;
printf("Float: %.2f\n", u.fval);
return 0;
}
Пример практического применения (обмен байтами):
#include <stdio.h>
#include <stdlib.h>
int main() {
char temp;
union {
unsigned char p[2];
unsigned int t;
} type;
printf("Enter a number: ");
scanf("%d", &type.t);
printf("Original: %d = %x hex.\n", type.t, type.t);
// Обмен двух младших байтов
temp = type.p[0];
type.p[0] = type.p[1];
type.p[1] = temp;
printf("Swapped bytes: %d = %x hex.\n", type.t, type.t);
return 0;
}
Объединения полезны для работы с данными разных типов, когда необходимо экономить память или интерпретировать байтовое представление числа.
Заключение
- Структуры в Си позволяют моделировать сложные объекты, объединяя различные типы данных.
- Указатели на структуры и динамическое выделение памяти открывают возможности для создания гибких динамических структур данных (списки, деревья, графы).
- typedef упрощает работу с пользовательскими типами.
- Передача структур в функции может выполняться как по значению, так и по указателю – выбор зависит от задачи.
- Перечисления (enum) и объединения (union) расширяют возможности языка, делая код более понятным и экономичным по памяти.
Практика написания программ с использованием этих конструкций является единственным способом глубокого освоения языка Си. Рекомендуется регулярно создавать небольшие проекты и эксперименты, чтобы закрепить полученные знания.