Similar presentations:
Multi-File Programs
1. Многофайловые программы
2. Монолитная программа
• Ситуация: код большой (500800+ строк) размещается водном файле
• Проблемы:
• сложно найти нужный код
• легко что-то сломать
• невозможно работать в команде
• всё перемешано
• Итог: файл превращается в
«спагетти-код»
// tool.c (1200 строк)
int main() { ... }
void ReadFile(...) { ... }
void WriteFile(...) { ... }
void Dump(...) { ... }
void Encrypt(...) { ... }
void Decrypt(...) { ... }
void Parse(...) { ... }
void Validate(...) { ... }
...
3. Проблема переиспользования
• Нужно использовать код повторно• program1.c → есть Dump()
• program2.c → копируем Dump()
• program3.c → снова копируем Dump()
• Что происходит:
• дублирование кода
• ошибки копируются
• сложно обновлять
• Пример:
• нашли баг в Dump()
• исправляем в program1.c
• забыли исправить в program2.c
• программа работает неправильно
• Copy–paste — не масштабируется
4. Модульная организация программ в C
// dump.h – Интерфейсная часть модуля#ifndef DUMP_H // Защита от повторного включения
#define DUMP_H
void DumpMemory(const void *data, size_t size);
#endif
// dump.c – Реализация модуля
#include "dump.h"
void DumpMemory(const void *data, size_t size) {
...
}
Преимущества
• код разделён по
назначению
• модуль можно
использовать в
нескольких программах
• проще сопровождать и
расширять программу
• интерфейс отделён от
//реализации
main.c
#include "dump.h"
int main(void) {
...
DumpMemory(buffer, size);
}
5. Структура модуля в C
// point.h#ifndef POINT_H
#define POINT_H
// point.c
#include "point.h"
#include <math.h>
typedef struct {
int x;
int y;
} Point;
void MovePoint(Point* p, int dx, int dy) {
p->x += dx;
p->y += dy;
}
void MovePoint(Point* p, int dx, int dy);
double GetDistance(Point p1, Point p2) {
int dx = p2.x - p1.x;
int dy = p2.y - p1.y;
return sqrt(dx * dx + dy * dy);
}
double GetDistance(Point p1, Point p2);
#endif // POINT_H
6. Почему нельзя просто разделить на .c файлы?
// point.ctypedef struct {
int x;
int y;
} Point;
void MovePoint(Point* p, int dx, int dy) { ... }
double GetDistance(Point p1, Point p2) { ... }
// main.c
int main(void) {
Point p = {0, 0};
MovePoint(&p, 1, 2);
}
// ошибка unknown type name 'Point
// ошибка implicit declaration of function 'MovePoint'
7. Как надо и как не надо делать
❌// main.c
typedef struct { int x; int y; } Point;
void MovePoint(Point*, int, int);
double GetDistance(Point, Point);
int main(void) {
Point p = {0, 0};
MovePoint(&p, 1, 2);
}
• Дублирование кода
• Риск ошибок
• Сложно поддерживать
• Как надо делать
• Вынести объявления в
отдельный файл .h
• #include "point.h"
• Заголовочный файл — это
единое место с
объявлениями, доступными
всем модулям
8. Сборка многомодульной программы
• Исходные файлы• main.c
point.c
point.h
• Этап 1. Компиляция исходных файлов
• GCC
gcc -c main.c
gcc -c point.c
• Clang
clang -c main.c
clang -c point.c
• GCC
gcc main.o point.o -o program
• Clang
clang main.o point.o -o program
• MSVC
cl main.obj point.obj /Fe:program.exe
• Можно выполнить оба этапа одной
командой
• GCC
• MSVC
• Этап 2. Компоновка объектных
файлов
cl /c main.c
cl /c point.c
gcc main.c point.c -o program
• Clang
clang main.c point.c -o program
• MSVC
cl main.c point.c /Fe:program.exe
• Каждый .c файл компилируется
отдельно,
затем объектные файлы объединяются
в одну программу
9. Компилятор и линкер
• Компилятор• Обрабатывает один .c
файл за раз
• Что делает:
• проверяет синтаксис
• проверяет типы
• преобразует код →
объектный файл (.o /
.obj)
• main.c → main.o
point.c → point.o
• Линкер (компоновщик)
• Объединяет объектные
файлы в программу
• Что делает:
• связывает функции между
файлами
• находит реализации по
именам
• формирует исполняемый
файл
• main.o + point.o →
program
10. Область видимости между файлами
• По умолчанию (global)• // point.c
int counter = 0;
• Доступ из другого файла
• // main.c
extern int counter;
• Ограничение области
видимости: static
• // point.c
static int counter = 0;
static void helper() {
... }
• Скрывайте всё, что не
является частью
интерфейса
Ключевое
слово
Область видимости
(нет)
Весь проект
static
Один .c файл
extern
Доступ из других
файлов
11. Применение статических функций
#ifndef MATH_UTILS_H#define MATH_UTILS_H
int Abs(int x);
#endif
#include "math_utils.h"
// Вспомогательная функция
static int Sign(int x) {
return (x > 0) - (x < 0);
}
// Публичная функция
int Abs(int x) {
return x * Sign(x);
}
• Abs — часть интерфейса (доступна извне)
• Sign — внутренняя деталь реализации
12. Проблема коротких функций
• Короткие функции — не всегда эффективны• int Abs(int x) {
return (x < 0) ? -x : x;
}
• Проблемы
• вызов функции → дополнительные инструкции
• возврат из функции требует дополнительных инструкций
• Для коротких функций:
• время вызова может быть сопоставимо с временем выполнения самой
функции
• Хотим:
• сохранить модульность
• уменьшить накладные расходы
• упростить код
13. Почему нельзя просто писать функции в .h
• Наивное решение: поместить определениефункции в .h
• Проблема
• main.c содержит Abs
other.c содержит Abs
• После компиляции:
• main.obj содержит Abs
• other.obj содержит Abs
• Ошибка компоновки:
• Multiple definitions of Abs
• Причина
• #include копирует код
• ункция попадает в каждый .c файл
• линкер видит несколько одинаковых
реализаций
• Обычные функции нельзя определять в .h
(если заголовок подключается в
несколько файлов)
// math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int Abs(int x) {
return (x < 0) ? -x : x;
}
#endif
// main.c
#include "math_utils.h"
// other.c
#include "math_utils.h"
14. inline-функции
• Тело функции доступно в местеиспользования
• Компилятор может встроить функцию
• Вместо
int y = Abs(n);
• Компилятор может встроить тело
int y = (n < 0) ? -n : n;
• Важный момент
• Встраивание происходит не из-за inline, а
потому что компилятор видит тело функции
• Наблюдение
• функция в .h → тело видно всем .c
• функция в том же .c → тоже может быть
встроена
• inline:
• уменьшает накладные расходы
• упрощает код для коротких функций
• даёт компилятору больше возможностей
оптимизации
// math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
inline int Abs(int x) {
return (x < 0) ? -x : x;
}
#endif
15. Скрытые типы (opaque types)
• Мы хотим скрыть отпользователя
внутреннее устройство
структуры
• Обычный подход
позволяет менять
свободно поля
// point.h
структуры:
typedef struct {
int x;
int y;
} Point;
• Подход с сокрытием:
структура объявлена,
но не определена
// point.h
typedef struct Point Point;
Point* CreatePoint(int x, int y);
void MovePoint(Point* p, int dx, int dy);
void DestroyPoint(Point* p);
16. Где находится определение структуры?
#ifndef POINT_H#define POINT_H
typedef struct Point Point;
Point* CreatePoint(double x, double y);
void DestroyPoint(Point* point);
void MovePoint(Point* point,
double dx, double dy);
#endif
#include "point.h"
#include <assert.h>
struct Point {
double x;
double y;
};
Point* CreatePoint(double x, double y) {
Point* point = (Point*)malloc(sizeof(Point));
if (point) {
point->x = x;
point->y = y;
}
return point;
}
void DestroyPoint(Point* point) { free(point); }
void MovePoint(Point* point, double dx, double dy) {
assert(point != NULL);
point->x += dx;
point->y += dy;
}
17. Что это даёт?
• Пользователь НЕ может:• Создать Point напрямую
• Обращаться к ее полям
• Пользователь может:
• Вызывать функции, объявленные в модуле
• Преимущества
• скрыта внутренняя реализация
• нельзя нарушить инварианты структуры
• можно менять структуру без изменения интерфейса
• меньше зависимостей между модулями
• Модуль управляет своими данными пользователь
работает через функции
18. Заключение
• Что мы узнали• программа может состоять из нескольких файлов
• код делится на модули (.c + .h)
• .h — интерфейс, .c — реализация
• компилятор работает с файлами отдельно
• линкер объединяет программу
• Хороший модуль
• скрывает реализацию
• предоставляет понятный интерфейс
• минимизирует зависимости
• может использоваться повторно
• Инкапсуляция в C
• static — скрытие внутри файла
• opaque types — скрытие структуры данных
• управление доступом через функции
programming