Многофайловые программы
Монолитная программа
Проблема переиспользования
Модульная организация программ в C
Структура модуля в C
Почему нельзя просто разделить на .c файлы?
Как надо и как не надо делать
Сборка многомодульной программы
Компилятор и линкер
Область видимости между файлами
Применение статических функций
Проблема коротких функций
Почему нельзя просто писать функции в .h
inline-функции
Скрытые типы (opaque types)
Где находится определение структуры?
Что это даёт?
Заключение
110.66K
Category: programmingprogramming

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.c
typedef 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 — скрытие структуры данных
• управление доступом через функции
English     Русский Rules