Если вы что-либо программируете, то рано или поздно ваши проекты становятся настолько большими, что хранить весь код в одном файле оказывается неудобно. В языке С++ при этом используется раздельная компиляция. В статье описывается маршрут сборки проекта, состоящего из нескольких файлов исходного кода и особенности использования заголовочных файлов.
Вообще, статью я решил написать потому, что раздельную компиляцию очень часто (авторы книг по С++, в том числе) сравнивают с системой модулей (которые есть в других языках). Однако это некорректное сравнение. В следующей статье я опишу как в С++ реализовать более-менее нормальные модули с помощью пространств имен, но сначала надо разобраться с раздельной компиляцией.
1 Назначение раздельной компиляции:
Хранить весь свой исходный код можно в одном файле, однако обычно говорят о следующих причинах разделять его: логическое разделение кода на части упрощает восприятие проекта и поиск нужного фрагмента;
проще отслеживать зависимости между файлами, чем между элементами программы в одном файле — это важно при документировании проекта;
Как разделить экран в Windows 10 на части в компьютере и ноутбуке
системы генерации документации по исходному коду (типа doxygen/JavaDoc ) позволяют писать пометки к отдельным файлам [1];
разделение кода на файлы может значительно ускорить компиляцию, т.к. при внесении изменений достаточно проводить трансляцию только той части кода, которую затронули изменения. Есть множество других причин, которые я не считаю достаточно существенными — например, Страуструп [2] пишет что раздельная компиляция упрощает командную работу (если каждый программист работает в своем файле), но это не совсем актуально при использовании современных систем контроля версий (таких как git ). Тем более устаревшими выглядят рассуждения о средах разработки, способных открывать в один момент только один файл с кодом.
2 Маршрут компиляции в С++:
Итак, в С++ мы можем легко добавить в проект несколько .cpp файлов (в IDE типа Visual Studio это делается с помощью нескольких кликов мышкой), написать в них какой-то код и скомпилировать. Однако, как функции одного файла могут использовать функции из другого? Как правильно разделить проект на файлы? — для ответа на вопросы нужно разобраться с процессом компиляции.
Файлы с кодом по-отдельности подаются на препроцессор, который обрабатывает такие директивы, как #include , #define и т.п. (про них мы поговорим позже). То, что формируется на выходе препроцессора называется единицей трансляции (для каждого .cpp файла будет сформирована своя единица трансляции), которая представляет собой код .cpp -файла, который мог быть чем-то дополнен (или наоборот — убрано лишнее). Транслятор преобразует единицы трансляции в объектные файлы, которые фактически содержат команды процессора, но не могут быть непосредственно выполнены, т.к. если один из них использует функцию из другого файла — то объектный файл содержит лишь объявление этой функции, а код функции размещен в другом объектном файле. Объединением этих файлов занимается линкер (компоновщик).
Как разделить файл на части программой WinRAR
3 Использование функции из других файлов. Объявления и определения. Пример:
Допустим, программа должна обрабатывать список студентов и выбирать из них двоечников, а также — список преподавателей с целью составления расписания консультаций (для двоечников). Не вдаваясь в детали, мы можем выделить относительно слабо связанные части в программе — составление расписание преподавателей и обработка списка студентов, такие части целесообразно разместить в различных файлах исходного кода: Пусть в файлах буду определены структуры успеваемости студента и расписания преподавателя, а также функции выполняющие соответствующие операции.
// create_expel_list.cpp enum Stuff < // преподаваемая дисциплина Literature, Mathematic >; struct Grade < // оценка Stuff stuff; enum < Excellent, Good, Acceptable, Bad >value; >; struct Student < string name; vectorgrades; >; vector create_expel_list(const vector /* из набора студентов выбирает таких, что среди оценок имеют двойки */ >
// create_timetable.cpp enum Stuff < // преподаваемая дисциплина Literature, Mathematic >; struct Prof < // преподаватель string name; Stuff stuff; >; struct Lesson < // занятие Stuff stuff; Prof prof; Time time; >; vector create_timetable(const vector students) < /* принимает список двоечников и преподавателей каким-то образом планирует расписание занятий (набор уроков) */ >
- структура Stuff объявлена в обоих файлах, т.к. она в них используется — без этого не сможет отработать транслятор (выдаст ошибку), ведь он обрабатывает единицы компиляции независимо друг от друга. Проблема возникнет если в одном из файлов мы добавим в структуру дополнительную дисциплину (например физику), а во втором забудем. Кроме того, в файле main.cpp нам также придется объявлять эту структуру (как и все остальные структуры), ведь именно там мы будем формировать список преподавателей;
- функция create_timetable принимает на вход набор студентов, однако в файле отсутствует объявление структуры студента — ее нужно добавить точно также (с теми же проблемами), как структуру Stuff ;
- в файле main.cpp мы хотим вызывать функции из других файлов, однако еще при трансляции мы получим ошибки если не объявим эти функции в текущем файле;
//main.cpp /* объявления всех структур — студента, предмета, . */ /* прототипы функций (лишь объявления, которые говорят транслятору, что где-то эти функции есть, чтобы он не ругался):*/ vector create_timetable(const vector students); vector create_expel_list(const vector // ввод списков студентов и преподавателей, // вызов функций create_expel_list и create_timetable
Теперь появляется новая проблема — если у какой-либо из функций мы изменим список параметров, а в прототипах main.cpp забудем — то ошибка не будет получена при трансляции, но при компоновке будет выявлено, что нужной функции в файлах нет. Ошибки линкера крайне неинформативны, в них очень сложно разобраться (линкер не может указать конкретную строку кода в файле с ошибкой, т.к. ошибки он выявляет лишь после соединения файлов в исполняемый файл).
Для решения всех перечисленных проблем в языке С++ используется директива #include «имя файла» , которая на этапе препроцессорной обработки заменяется на содержимое подключаемого файла, а также применяется ряд других директив.
4 Обработка директив препроцессора в С++
4.1 Директива #include
В языке С++ есть ряд директив, которые обрабатываются препроцессором. Файлы исходного кода в С++ разделяются на заголовочные ( .h , .hpp ) и файлы реализации ( .cpp ). В заголовочных файлах обычно описывают прототипы функций, определения структур, объявление данных (общих переменных), определение констант, а в файлах реализации размещают реализацию функций. Так делают потому, что один один заголовочный файл может быть включен в несколько разных единиц трансляции и если он будет содержать реализацию функции, то на этапе компоновки мы получим ошибку. Приведенный выше пример с использованием заголовочных файлов можно было бы переписать следующим образом:
// create_expel_list.h enum Stuff < // преподаваемая дисциплина Literature, Mathematic >; struct Grade < // оценка Stuff stuff; enum < Excellent, Good, Acceptable, Bad >value; >; struct Student < string name; vectorgrades; >; vector create_expel_list(const vector
// create_expel_list.cpp #include «create_expel_list.h» vector create_expel_list(const vector /* из набора студентов выбирает таких, что среди оценок имеют двойки */ >
// create_timetable.h #include «create_expel_list.h» struct Prof < // преподаватель string name; Stuff stuff; >; struct Lesson < // занятие Stuff stuff; Prof prof; Time time; >; vector create_timetable(const vector students);
// create_timetable.cpp #include «create_timetable.h» vector create_timetable(const vector students) < /* принимает список двоечников и преподавателей каким-то образом планирует расписание занятий (набор уроков) */ >
//main.cpp #include «create_timetable.h» // ввод списков студентов и преподавателей, // вызов функций create_expel_list и create_timetable
В результате применения директивы #include отпадает необходимость вручную в каждом файле, использующем какие-либо функции или структуры из другого прописывать объявления, мы выносим все объявления в заголовочный файл и подключаем сразу всех их одной строчкой. Теперь зависимости между файлами будут следующими:
4.2 Директивы #define и #ifndef . Стражи включения
В рассмотренном выше примере файл main.cpp включает в себя только create_timetable.h , хотя и использует структуры из create_expel_list.h — этого оказывается достаточно так как create_timetabe.h содержит включение create_expel_list.h (которое заменяется текстом файла). Библиотеки часто поставляются именно в виде набора заголовочных файлов и файлов реализации, однако программист, использующий библиотеки не должен знать как они устроены внутри и какие зависимости существуют между файлами. При этом, даже если в нашей программе в main.cpp подключить create_expel_list.h — мы получим ошибку при компиляции, т.к. одна и та же структура окажется объявленной дважды. Для решения проблемы применяются так называемые «стражи включения».
Директива #define позволяет определить лексему (имя макроса) и строку, на которую она будет заменена препроцессором:
#define PI 3.1415
Директива #ifndef используется для проверки того, был ли определен макрос с заданным именем:
// create_timetable.h #ifndef CREATE_TIMETABLE_H #define CREATE_TIMETABLE_H #include «create_expel_list.h» struct Prof < // преподаватель string name; Stuff stuff; >; struct Lesson < // занятие Stuff stuff; Prof prof; Time time; >; vector create_timetable(const vector students); #endif
Фрагмент кода, помещенный между #ifndef и #endif будет пропущен компилятором если макрос с именем, переданным #ifndef уже был объявлен. Внутри этого фрагмента размещается соответствующее объявление макроса и всё содержимое заголовочного файла. В результате, при повторном включении файла (что очень часто бывает при наличии иерархических зависимостей между файлами) — его содержимое будет добавлено лишь один раз.
Стражи включения должны быть прописаны в каждом заголовочном файле. Часто в качестве имени макроса стража включения выбирают имя соответствующего файла.
Дополнительная литература по теме:
- Работа с системой автоматической генерации кода doxygen на примере игры «Сапер».- URL: https://pro-prof.com/archives/887
- Страуструп Б. Язык программирования C++. 3-е издание. — М.: Бином, 1999. — 991 с.
Источник: pro-prof.com
Деление программы на модули
Программу можно разбить на несколько исходных файлов, которые называют модулями. Каждый из модулей можно компилировать отдельно, а затем объединить их все. Процесс объединения отдельно скомпилированных модулей называется связыванием или компоновкой (linking).
Разделение программ на несколько небольших, удобных для редактирования
и управления частей имеет определенные преимущества.
Во-первых, это уменьшает время компиляции.
Пересобирать программу целиком каждый раз, когда была изменена одна функция, очень неудобно. Намного проще откомпилировать одну функцию, организованную в виде отдельного модуля (можно составить модуль и из нескольких функций).
Во-вторых, гораздо проще понимать, писать и отлаживать программу, состоящую из нескольких хорошо продуманных модулей, каждый из которых логически объединяет несколько связанных между собой функций. Крайне трудно разобраться в содержимом одного объемистого модуля. По мере разрастания модуль становится все более запутанным и его приходится неоднократно переписывать.
Последний (и, наверное, самый главный) аргумент заключается в том, что небольшие модули можно использовать и при написании других программ.
Пример. Функция FunctionDemo. cpp содержит следующий код:
// FunctionDemo — демонстрирует использование функций
// путем выделения внутреннего
// цикла программы NestedDemo
// в отдельную функцию
// sumSequence — суммирует последовательность введенных
// с клавиатуры чисел, пока
// не будет введено отрицательное число.
// Возвращает сумму введенных чисел
int nAccumulator = 0 ;
// Ожидание следующего числа
// Если число отрицательное.
// . тогда выполнить выход из цикла
// . в противном случае добавить число
nAccumulator = nAccumulator + nValue;
int main(int nArg, char* pszArgs[])
// накопление последовательности чисел.
// сложить числа, введенные
// вывести полученный результат
// . . . пока сумма не равна 0
> while (nAccumulatedValue != 0 ) ;
В FunctionDemo суммируется последовательность введенных с клавиатуры чисел. Однако, в отличие от других программ, для сложения введенных чисел main () вызывает функцию sumSequence ().
Разделение программы
Модуль FunctionDemo.cpp можно логически разделить на две функции, которые выполняют разные действия. Сейчас функция main() приглашает пользователя ввести числа и выполняет цикл, в котором накапливаются и выводятся суммы последовательностей чисел.
Функция sumSequence() суммирует последовательность чисел и возвращает полученный результат.
Таким образом, программу можно разделить на модули, один из которых будет содержать функцию, выполняющую сложение, а второй — использовать эту функцию для сложения последовательности чисел и вывода суммы.
Для демонстрации этого метода новая версия программы FunctionDemo разбита на два модуля: первый содержит функцию sumSequence(), а второй — функцию main().
Хотя sumSequence() можно разделить еще на несколько модулей, разбивать FunctionDemo не имеет смысла ни для упрощения работы, ни для ускорения компиляции. Этот пример демонстрирует механизм разбиения программы на несколько частей и не более того.
Источник: studfile.net
Как правильно выносить код в разные файлы?
Доброго времени суток.
Подскажите пожалуйста, как правильно выносить код в разные файлы в c++?
Пишу программу, она уже разрослась на 3000+ строк кода и всё это в одной cpp файле!
Как мне правильно разделить код в разные файлы?
Попутный вопрос: было бы здорово вынести код в отдельные dll, но насколько это сложно и где это почитать?
- Вопрос задан более трёх лет назад
- 13015 просмотров
Комментировать
Решения вопроса 0
Ответы на вопрос 6
Сложность этого действия зависит от двух факторов:
— от качества кода на данный момент
— от качества того, что хотите получить на выход
По первому пункту, если у вас код грамотно разбит на классы и/или функции, то проблемы быть не должно. Как уже писали, просто вынесите все определения в хедеры, все остальное оставьте в .cpp . С другой стороны, я себе слабо представляю как написать грамотный код в одном файле.
Это приводит нас ко второму пункту. Для того, чтобы в итоге получился качественный продукт, необходимо логическое разделение на подзадачи. Если это выполнить грамотно это приведет к небольшим функциям, которые всегда делают что-то одно и/или к небольшим классам, которые отвечают за одну сущность. Такую структуру легко вынести в разные файлы.
Если же у вас много глобальных переменных и тп, то код тоже можно легко разнести в разные файлы, но с группировкой по файлам будет сложно, да и смысла в этом много не будет.
ПС. Не уверен, знаете ли вы, но не забывайте в каждом .h файле:
#ifndef MY_AWESOME_HEADER_H #define MY_AWESOME_HEADER_H // код вашего файла #endif
это убережет вас от последущих повторных включений этого файла.
Ответ написан более трёх лет назад
Комментировать
Нравится 2 Комментировать
Знаю JS, PHP, C++, C#
Несложно.
Определитесь сначала с модулями и интерфейсами.
В .h файлы выносите сигнатуры и типы, реализация — в cpp файлах, в которые приинклюжен нужный h файл.
с дллками легко — нужнен только префикс declspec(dllexport) для экспортируемых и declspec(dllimport) для импортируемых, но есть макрос, заменяющийся на это автоматом
Ответ написан более трёх лет назад
Нравится 1 1 комментарий
Это действует, если человек без шаблонов пишет. С шаблонами и разделением на .h и .cpp сложнее.
Веду кружки по робототехнике
Ответ написан более трёх лет назад
Комментировать
Нравится 1 Комментировать
Вам нужен рефакторинг. То, что Вы описали, говорит о том, что Ваш код далёк от совершенства.
Вам нужно для начала применить extract method, затем exctract class или подобные методы. После чего можно будет спокойно классы разнести по отдельным файлам.
Ответ написан более трёх лет назад
Комментировать
Нравится Комментировать
System programming, Reversing Engineering, C++
А для чего нужно разбивать на несколько DLL ? Может просто переоформить код,т.е провести рефакторинг и оставить все как есть в рамках одной DLL?
Я бы Вам посоветовал Пока «жить» в рамках одной DLL и провести рефакторинг, тогда внешняя программа что использует вашу DLL послужит хорошим тестовым стендом и провести проверочное тестирование после рефакторинга будет значительно проще! Вторым этапом, если Вы все же решите разбить на несколько DLL Вам будет значительно проще,т.к. понятный код и он протестирован!
Разбивается путем мышления и задавание себе вопросов.
Каждый модуль обязан отвечать утвердительно на вопрос «Он действительно решает только одну задачу?». При этом надо понимать не примитивные задачи «чтение из файла» или «подсчитать энтропию», под «одной задачей» понимает один пункт взятый с уровня абстракции.
Пример:
Уровень 1: Чтение настроек
Под-уровень 1: Формирование имени файла с настройками
Под-уровень 2: Открытие и чтение из файла с настойками
Под-уровень 3: Задание глобального объекта конфигуратор соглассно прочитанным настройками
и т.д. и т.п.
В любом случае идеальных методик по разбиению нет! Вас никто не научит программировать, это процесс итеративный, сегодня лучше чем вчера, а завтра будет еще лучше чем сегодня 😉
Ответ написан более трёх лет назад
Комментировать
Нравится Комментировать
Пишу комментарии в комментарии, а не в ответы
Для начала нужно понять идею dll. В них есть нужно размещать готовые самодостаточные компоненты, которые имеют смысл и за пределами узкой специфики одного проекта или для поддержки модульности (плагины, например). Грубо говоря, нужно думать, есть ли в этом великий смысл? Если просто хочется логически разделить код программы, то для этого будет достаточно разделения на уровне исходного кода. Об остальном сказано выше.
Ответ написан более трёх лет назад
Комментировать
Нравится Комментировать
Ваш ответ на вопрос
Войдите, чтобы написать ответ
- C++
Как исправить код сортировки по алфавиту StringGrid в c++ builder rad studio?
- 1 подписчик
- 5 часов назад
- 23 просмотра
Источник: qna.habr.com