Страницы

Поиск по вопросам

понедельник, 25 ноября 2019 г.

Для чего нужны Header файлы в С++? Почему нельзя писать без них?


Весь гугл перерыл, не могу понять. И википедию перечитал и вообще все что угодн
перечитал. Правда не понимаю.

Что мешает подключать просто .cpp файлы?

Ну подключил ты его два раза, ну пусть компилятор только один раз подключает и все
раз он один черт все в один как бы файл склеивает, значит первое подключение будет выш
остальных и будет видно им внизу. Пусть смотрит время изменения и перекомпилирует только изменившееся. Пусть автоматически генерирует и прикрепляет header файлы с описанием интерфейсов к откомпилированному бинарнику и т.п.

Иными словами, почему рутиная работа по генерации хедер файлов, не автоматизирован
и возложена на разработчика? Ведь компилятор рядом с откомпилированным бинарником может легко сам сгенерировать файл описания интерфейсов (который он получил, сканируя cpp файл).

Можете привести ситуацию, в которой бы возникали ПРОБЛЕМЫ без использования хедер файлов? Это будет самое лучшее обьяснение.
    


Ответы

Ответ 1



Проблема лежит в области обратной совместимости. Посмотрите, любой новый язык программирования — да даже Паскаль, не говоря уже Java или C# — не нуждаются в заголовочных файлах. Без сомнения, C++ тоже мог бы обойтись без них. В чём же дело? Перенесёмся на полвека назад, в 1972 год. Представим себе компилятор языка C. Допустим, мы хотим написать дизайн компилятора. Мы не можем скомпилировать всю программ за раз, у нас на это просто не хватит памяти. Компьютеры тогда были маленькими и медленными. Мы хотим компилировать программу по кусочкам, по нескольку функций за раз. У нас сразу же возникает проблема: как скомпилировать функцию f, которая ссылаетс на другую функцию g? Нам нужно отдельное описание других функций. Мы могли бы, конечно прочитать все исходные файлы, для начала выяснить, какие функции у нас есть, и зате прочитать их второй раз и скомпилировать один за одним. Но это было слишком сложно и медленно, нужно было парсить определения функций дважды, и один раз выбрасывать результат! Это недопустимый расход процессорного времени! Плюс если держать в памяти определения всех функций, может снова-таки не хватить памяти. На кого Деннис решил возложить сложную проблему отделения описания функции от е реализации, и подключения только нужных описаний при компиляции данной функции? На нас программистов. Он решил, что мы должны сами помочь компилятору и скопипастить определения функций в отдельный файл, и сами указать компилятору, какие файлы с определениями нужны. (То есть первый шаг компиляции возлагается на нас.) Это радикально упростило компилятор, но привело в свою очередь к проблемам. Что будет если мы забыли подключить нужный заголовочный файл? Ответ: ошибка компиляции. Что будет если смысл текста заголовочного файла меняется в зависимости от какого-нибудь макроса? Ответ: компилятор «тупой», и не пытается детектировать эту проблему, он перекладывает ответственность на нас. На момент разработки языка это было правильное решение. Компилятор оказался практичным быстрым, и программисты были не прочь помочь компиляции. Ну и если кто допускал ошибку, сам был виноват. Перемотаем стрелки часов в 1983 год. Бьярн создаёт C++. Он решил взлететь на волн популярности языка C, и перенял модель компиляции C с отдельными translation unit'ам и связанными с этим проблемами прямо из C. Впрочем, первые версии C++ были просто препроцесооро языка C! Поэтому проблемы раздельной компиляции перекочевали из C в C++. Хуже того, добавились новые проблемы. Например, шаблоны классов выглядят как классы, но не генерируют объектного кода сами по себе, поэтому для них приходится идти на ухищрения и обходить недостатки системы раздельной компиляции (например, включением реализации в header и трюками компоновщика). А дальше вступила в игру обратная совместимость. Сейчас, в 2017 году, есть так мног кода, написанного в стиле «с заголовками», и так много кода исходит из различных тонкостей, связанных с этим, что менять парадигму уже поздно, поезд практически уехал. Впрочем, существует проект модульной системы в C++, который должен помочь программиста избавиться от наследства полувековой давности. Он ещё не реализован, и в нём есть сложност уровня дизайна (например, в header'е был определён макрос, будет ли он виден, если мы перейдём от header'ов к модулям?) Надеюсь, в будущем разработчики языка таки смогут побороть проблему обратной совместимости.

Ответ 2



Сборка программы происходит в три этапа: препроцессор, компилятор и компоновщик. Препроцессор, обрабатывая директиву #include "file", подставит содержимое file текущий файл. Получается, если вы включите cpp файл в несколько компилируемых файлов, то он будет скомпилирован несколько раз. И вот тут вступает в силу Правило одного определения Заодно, почитайте, что такое Единица трансляции. Приведу пример: ClassA.cpp class ClassA { public: void someFunction() {}; } ClassB.cpp #include "ClassA.cpp" class ClassB { } ClassC.cpp #include "ClassA.cpp" class ClassC { } После выполнения препроцессора у вас ClassB.cpp и ClassC.cpp соответственно превратятся в class ClassA { public: void someFunction() {}; } class ClassB { } и class ClassA { public: void someFunction() {}; } class ClassC { } И только теперь начинается компиляция. Даже если вы не будете отдельно компилироват ClassA.cpp, то все равно нарушите правило одного определения (ClassA определен в двух единицах трансляции - ClassB.cpp и ClassC.cpp).

Ответ 3



Так сложилось исторически. Использование автоматически сгенерированных заголовочны файлов требует непростой сборочной системы, которая бы умела определять порядок компиляции при наличии сложных зависимостей между модулями. А поскольку на момент становления языка таких систем еще не придумали - то добавлят генерацию заголовочного файла в компиляторы не стали. И до сих пор такой системы не появилось потому что компиляторы не умеют генерировать заголовочные файлы. Возможно, модули из нового стандарта все изменят. В любом случае, заголовочные файлы останутся для тех случаев, когда импорт .cpp-файл неприменим ни в каком виде. Например, когда .cpp-файла и нет - при подключении сторонне библиотеки. Или при наличии циклической зависимости между модулями. Или просто в ситуации, когда на заголовочный файл налагаются дополнительные требования, делающие его автоматическую генерацию недопустимой.

Ответ 4



Ожидается что в C++17 появится первый стандартизировнный вариант модулей, то ест поддержки сборок без заголовочных файлов. Насколько я понял в этом варианте модули будут реализованы как расширение компилятора, то есть это фнукция будет необязательной в C++17. Однако уже сейчас модули реализованы в Visual Studio 2015 Update 1 и clang, так что их можно попробовать.

Ответ 5



Что мешает подключать просто .cpp файлы? Отсутствие в них информации о типах, доступной извне. Компилятор превращает файлы исходных кодов (.c и .cpp) в объектные файлы (.obj, сшиваемы позднее компоновщиком в единый .exe или .dll файл) — «полуфабрикаты»-«чёрные ящики», импортирующие и экспортирующие символы. Символ, в свою очередь, — это совокупность «имя функции/переменной — её смещени относительно начала объектного файла». Импорт — это зависимости объектного файла (при этом неважно, где они будут реализованы, требуется лишь совпадение имён); экспорт — это то, что в этом файле реализовано. Таким образом, объектные файлы не содержат типов данных и прочих абстракций, существующи исключительно в воображении компилятора. Только машинный код и данные, существующие в виде реально выделяемого места в ОЗУ, и именованные метки. К сожалению, одного только знания имени недостаточно для формирования импорта (зависимости) Компилятору необходимо знать ещё структуру принимающих участие типов для генерации корректных смещений на стеке, вставки дополнительных вызовов конструкторов/деструкторов, выполнения неявных преобразований типов, проверки корректности вызова и т. д. Так как этой информации в объектном файле нет (напомню, там только имена), она должн быть описана в виде объявлений в том файле исходных кодов, который эту внешнюю сущность хочет использовать. Описание можно выполнить либо в самом .cpp файле, либо вынести его в отдельный .hpp файл и подключать его всюду, где требуется описание типа. Почему же описание типов не встраивается в реализующий их объектный файл? Один тот же тип данных может использоваться в произвольном количестве мест, а потому подобно встраивание нарушает правило одного определения. Согласно этому правилу всякая сущность должна иметь только один источник (объектный файл), чтобы у компоновщика не было неопределённости при сопоставлении импортов и экспортов. Ну пусть компилятор только один раз подключает и все, раз он один черт все в оди как бы файл все склеивает, значит первое подключение будет выше остальных и будет видно им внизу. Сборка программы на C и C++ происходит в два этапа. Компилятор превращает каждый файл исходных кодов (.c и .cpp) в объектный файл (.obj) При этом каждый файл исходных кодов компилируется независимо, будто он один во вселенно и никого кроме него больше не существует. На этом этапе компилятор не видит других объектных файлов, но он уже обязан выдать готовый машинный код. Такой подход был выбран разработчиками языка для возможности распараллеливания компиляции. Компоновщик берёт указанные объектные файлы (неважно, как и когда они были получены), связывает их импорты и экспорты и объединяет в единый исполняемый (.exe или .dll) файл. Иными словами почему рутиная работа по генерации хедер файлов, не автоматизированн и возложенна на разработчика? Ведь компилятор рядом с откомпилированным бинарником может легко сам сгенерировать файл описания интерфейсов (который он получил сканируя cpp файл). Вот вам .cpp файл (без включения нестандартных заголовков, как хотели): #include #include Logger::Logger() : _console(std::cout) { } void Logger::log(const std::string& message) { _console << message; } Как компилятор выведет из этого структуру класса Logger? А именно: какие поля должн быть в классе и каков их модификатор доступа? Ведь реализация одного класса может быть распределена по нескольким файлам, ровно как и один файл может содержать реализацию части методов нескольких классов. Вы, наверное, скажете: «описать class Logger{...}; здесь же, чтобы из него генерировался .hpp». А в чём тогда разница, если мы это описание и так выносим в заголовочный файл? Можете привести ситуацию в которой бы возникали ПРОБЛЕМЫ без использования хедер файлов? myclass.cpp #include class Foo { int _value; public: Foo() : _value(rand()) { } // ... ещё много-много методов int bar() const { return _value; } } main.cpp // Копия простыни из foo.cpp #include class Foo { int _value; public: Foo(); // ... ещё много-много методов int bar() const; } int main() { Foo a; std::cout << a.bar(); return 0; } one-more-file.cpp // Ещё раз копия class Foo { int _value; public: Foo(); // ... ещё много-много методов int bar() const; } // ... ну и так далее для каждого файла, использующего Foo. А теперь представьте, что каждое изменение описания класса потребует массовой замен в большом количестве мест. Забудете исправить — компилятор промолчит, будучи введённый в заблуждение, на итоговая программа будет падать.

Ответ 6



Ну подключил ты его два раза, ну пусть компилятор только один раз подключает и все А если я не хочу один раз? Вот предположим, я генерирую за счёт макросов пачку похожих функций, где какие-т минимальные различия в поведении определяются другими макросами. Т. о. вместо генерации кода я меняю значения констант и подключаю один и тот же cpp-файл в другой cpp-файл. А ты хочешь меня этой возможности лишить? Вот пример: http://ideone.com/TlN4r0. Хотя есть лишние усложнения из-за того, чт приходится всё в один файл упихивать (ради ideone), но идея должна быть понятна. А ещё, предполагается, что функция достаточно большая, но различия достаточно маленькие. В таком случае помещение всей функции в макрос становится нерациональным. #ifndef MAIN #define MAIN #include using namespace std; #define D 1 #include __FILE__ #undef D #define D 2 #include __FILE__ #undef D int main() { cout << add1(10) << ' ' << add2(10) << endl; return 0; } #else #define ADD0(d) add##d #define ADD(d) ADD0(d) int ADD(D)(int x) { return x + D; } #undef ADD0 #undef ADD #endif

Комментариев нет:

Отправить комментарий