[БЕЗ_ЗВУКА] Когда
мы начинали разговор о разделении кода на несколько файлов,
то в качестве одного из недостатков хранения всего
кода в одном файле мы называли то,
что при минимальном изменении программы у нас она пересобирается вся,
в случае, если весь код лежит в одном файле.
Сейчас мы разделили код нашего проекта на целых четыре файла.
Но при этом каждый раз, когда мы меняем что угодно в нашем проекте,
он все равно пересобирается целиком.
Почему это происходит?
Потому что у нас есть файл coursera.cpp, в который так
или иначе включаются с помощью директивы #include три других наших файла.
Соответственно, если мы в них что-нибудь меняем, то они вставляются в наш
coursera.cpp, и вся программа перекомпилируется целиком.
Но давайте подумаем.
Например, есть у нас функции в "synonyms.h", которые умеют
работать со словарем синонимов — добавлять в него, проверять количество синонимов.
Есть эти функции и есть тесты на них.
Если мы внесем изменения в тесты, например,
добавим какой-нибудь еще тестовый случай,
вот у нас есть какой-то тест на TestCount, давайте там проверим,
что при
пустом словаре и для строки b у нас тоже вернется ноль.
Если мы поменяли тесты, то нам нет никакой необходимости
перекомпилировать сами функции, потому что у нас только тесты поменялись.
Но при этом мы не можем этого увидеть в консоли Eclipse,
но мы понимаем, что так как все файлы включаются в файл с решением,
то мы все равно перекомпилируем всё.
И нам нужно, видимо, что-то
опять же поменять в нашем проекте, чтобы все-таки достичь этой цели,
не пересобирать весь проект целиком при каждом изменении.
И чтобы нам научиться это делать, нам надо понять,
как происходит сборка проектов на C++,
которые состоят из нескольких файлов.
И чтобы нам это понять,
давайте обратим внимание на расширение файлов в нашем проекте.
Мы до этого пока как бы внимания им особо не уделяли,
а вот сейчас давайте посмотрим внимательно.
У нас есть файл test.h, есть файл synonyms.h,
test_runner.h и coursera.cpp.
У нас есть три файла с расширением .h и один с расширением cpp.
Наверное, это не случайно.
Давайте проведем вот такой эксперимент.
Возьмем наш проект и добавим в него еще один cpp файл.
Пока у нас только один, давайте добавим еще один.
Правой кнопкой на проекте New,
и вот здесь раньше мы выбирали Header File, давайте выберем Source File.
И назовем его, например, «еще один» — one_more.cpp.
Добавим в наш проект пустой файл с расширением cpp.
Теперь сделаем следующее: вычистим все результаты сборки, соберем.
И теперь, вот мы вычистили результаты сборки,
мы сейчас будем собирать наш проект с нуля.
Давайте его соберем.
Запустим сборку, она успешно завершилась.
И давайте откроем окно консоли и внимательно посмотрим,
какие команды выполнял Eclipse в процессе сборки нашего проекта.
Что мы здесь видим?
Мы видим, что компилятор g++ запускался трижды.
Первый раз — вот здесь это видно — он запускался для файла coursera.cpp.
Второй раз он запускался для нашего только что добавленного файла one_more.cpp.
В результате было получено два файла с расширением .o.
Вот этот параметр −o задает имя выходного файла,
поэтому по значению параметра −o мы можем понимать,
какие выходные файлы формировались в этой стадии.
И потом была третья стадия,
в которой на вход были поданы вот эти файлы с расширением o,
а на выходе получился исполняемый файл coursera.exe.
Этот пример демонстрирует, каким образом выполняется
сборка проектов на C++, состоящих из нескольких файлов.
И ее общая схема приведена на экране.
Сборка проектов состоит из трех стадий — это препроцессинг,
компиляция и компоновка, которую еще на жаргоне называют линковкой.
Собственно, что происходит?
Как мы уже видели, первая стадия — это препроцессинг,
когда выполняются все директивы include.
Дальше, после того как препроцессинг выполнен,
берется каждый отдельный cpp файл и компилируется.
В результате компиляции каждого cpp файла получается так называемый объектный файл.
Вот на схеме у нас объектные файлы изображены как файлы с расширением .o.
И затем начинается третья стадия — это стадия компоновки,
когда берутся все объектные файлы,
которые у нас получились, и компонуются в один исполняемый файл,
что у нас и приведено на схеме.
Хорошо.
Это мы сейчас увидели, как собираются файлы,
как собираются проекты, состоящие из нескольких файлов.
А теперь давайте сделаем вот такую вещь.
Мы только что собирали наш проект, видели,
какие этапы сборки он проходил, и сделаем мы сейчас такое.
Возьмем наш файл, вот этот пустой one_more.cpp,
и добавим в него комментарий, просто какой-то комментарий, неважно какой.
И снова запустим сборку.
Запускаем сборку.
И снова в консоли посмотрим, какие команды выполнялись.
Смотрите: у нас компилятор наш g++ был запущен уже два раза,
а мы видели, что при сборке с нуля он запускался трижды.
При этом мы видим, что выполнялась сборка только для файла one_more.cpp.
Файл coursera.cpp, где у нас лежит решение задачи,
не перекомпилировался, он не был тронут.
Мы перекомпилировали только one_more.cpp и дальше выполнили компоновку.
Это наталкивает на определенные мысли.
Давайте возьмем файл coursera.cpp и тоже добавим в него какой-нибудь комментарий.
Запустим сборку.
Смотрим в консоль: ага,
опять был перекомпилирован только один файл, на этот раз coursera.cpp, потому что
мы поменяли его, а файл one_more.cpp мы не трогали, и он не перекомпилировался.
И потом в конце, опять же, была линковка.
Давайте теперь сделаем еще такую вещь: возьмем заголовочный файл
synonyms.h и в него добавим какой-нибудь комментарий.
Скомпилируем.
Программа все еще компилируется.
И опять посмотрим, что у нас снова перекомпилировался coursera.cpp.
Но это и понятно, потому что synonyms.h включается в coursera.cpp,
поменялся включаемый файл, нужно перекомпилировать coursera.cpp целиком.
Какие выводы мы можем сделать?
Мы только что узнали, что при повторной сборке
проекта компилируются только измененные cpp файлы.
При этом внесение изменений в заголовочные файлы приводит к
перекомпиляции всех cpp файлов, в которые эти заголовочные файлы включаются.
А теперь давайте вспомним нашу исходную задачу.
Мы хотели сделать так, чтобы при минимальном
изменении нашего кода у нас не происходило пересборки всего проекта.
Как мы, обладая этими новыми знаниями, зная теперь,
как выполняется сборка больших проектов, как мы можем так поменять
наш код, чтобы он реже пересобирался?
Ответ вы уже видите на экране.
Мы можем взять определение
функций и методов классов и перенести их в cpp файлы.
То есть в заголовочных файлах мы оставляем только объявления,
а определения отправляем в cpp-файлы.
Что нам это даст?
Мы сделаем такую вещь: мы логически не связанные друг с
другом определения разнесем в отдельные cpp-файлы.
И тогда, когда мы меняем, например, определения тестов,
то определения функций, которые эти тесты покрывают,
не меняются, и соответственно, они не будут перекомпилироваться.
Таким образом мы минимизируем количество cpp-файлов,
которые нужно перекомпилировать при каждом изменении программы.
Давайте выполним такое преобразование с нашим проектом,
то есть вынесем определение в другие...
в cpp-файл.
И начнем вот, например, с test_runner.h.
Сделаем вот что.
Добавим в него, в наш проект файл test_runner
test_runner.cpp.
И вынесем в него определения.
Правда, здесь есть один небольшой нюанс.
У нас...
Мы пока что говорили про объявления и определения функций,
и даже делали объявления шаблонов.
Но вот определения шаблонов — это штука такая немножечко специфическая.
Поэтому сейчас мы определения шаблонов в cpp-файл переносить не будем.
Мы далее в наших курсах изучим и поймем, почему этого не надо делать.
Поэтому сейчас шаблоны мы трогать не будем, а вот, например,
фунцию Assert — это как бы обычная функция — мы перенесем в test_runner.cpp,
так же, как и деструктор класса TestRunner.
Итак, мы перенесли.
Запускаем компиляцию, и у нас не компилируется.
В файле test_runner.cpp мы видим, что файл не знает, что такое...
компилятор не знает, что такое AssertEqual.
Ну, понятное дело.
Потому что когда он компилировал вот этот вот cpp-файл, он ничего в него не включил.
И вот эти вот голые фукнции он пытается компилировать.
Чтобы у нас все нормально компилировалось,
мы подключим test_runner.h.
И теперь у нас все нормально компилируется.
Отлично.
Давайте сделаем это же преобразование и с другими файлами.
Вот у нас есть synonyms.h, так же берем и
добавляем synonyms.cpp.
Идем в synonyms.h, и вот у нас есть блок с объявлениями,
его не трогаем, а определения функций отправляем вот сюда.
Делаем #include "synonyms.h",
компилируем, и у нас компилируется.
Отлично.
Остался tests.h.
New, Source File,
tests.cpp, и все вот эти вот
определения, которые у нас есть,
мы отправляем вот сюда, в tests.cpp.
И делаем #include "tests.h"
Компилируем нашу программу и мы видим, что наш проект собрался.
А теперь давайте проверим, что выполняется, собственно, то свойство,
которое мы хотели обеспечить.
Давайте вот, например,
внесем ну какие-нибудь, неважно, какие, вот снова напишем комментарии.
Какие-то внесем изменения в tests.cpp.
И запустим сборку.
Смотрим в консоль и мы видим, что пересобран был только tests.cpp.
Другие файлы — coursera.cpp,
значит, synonyms.cpp — не были пересобраны.
У нас пересобрался только тот файл, который мы поменяли.
Давайте еще сделаем такой...
такое действие: давайте внесем изменение в test_runner.h.
Ну тоже добавим какой-нибудь в него комментарий и запустим сборку.
И у нас пересобралось все,
у нас пересобралось все, пересобрался test_runner.cpp, пересобрались tests.cpp,
а вот почему пересобралась coursera.cpp, непонятно.
Нужно посмотреть в нее, и становится очевидно, почему она пересобралась.
Потому что она включает в себя tests.h, а tests.h включает test_runner.
Поэтому изменение test_runner привело к перекомпиляции всех cpp файлов,
потому что он включается во все эти cpp-файлы.
Давайте подведем итоги.
Что мы узнали в этом видео?
Мы теперь знаем, что сборка больших проектов — проектов,
состоящих из нескольких файлов — состоит из трех стадий.
Это препроцессинг, компиляция и компоновка.
Мы знаем, что при повторной сборке проекта компилируются только измененные cpp-файлы.
И внесение изменений, внесение определений
в cpp-файлы позволяет нам при каждой пересборке компилировать
только изменившиеся cpp-файлы, а не целый проект.
И за счет этого мы можем существенно ускорять сборку наших проектов.