Зачем нужны файлы заголовков и файлы .cpp?

Почему в C++ есть файлы заголовков и файлы .cpp?

Связанный вопрос: stackoverflow.com/questions/1945846/…

Spoike 22.12.2009 14:45

это обычная парадигма ООП, .h - это объявление класса, а cpp - определение. Никто не должен знать, как это реализовано, он должен знать только интерфейс.

Manish Kakati 26.04.2017 14:29

Это лучшая часть С ++, отделяющая интерфейс от реализации. Это всегда хорошо, а не хранить весь код в одном файле, у нас есть разделенный интерфейс. Некоторое количество кода всегда присутствует, например, встроенная функция, которая является частью файлов заголовков. Выглядит хорошо, когда отображается файл заголовка, отображающий список объявленных функций и переменных класса.

Miank 07.06.2017 08:35

Бывают случаи, когда файлы заголовков важны для компиляции, а не просто предпочтения организации или способ распространения предварительно скомпилированных библиотек. Допустим, у вас есть структура, в которой game.c зависит от ОБОИХ Physics.c и math.c; Physics.c также зависит от math.c. Если бы вы включили файлы .c и навсегда забыли о файлах .h, у вас были бы дублированные объявления из math.c и никакой надежды на компиляцию. Это то, что для меня имеет наибольшее значение, почему файлы заголовков важны. Надеюсь, это поможет кому-то другому.

Samy Bencherif 06.07.2019 22:00

Я думаю, это связано с тем, что в расширениях разрешены только буквенно-цифровые символы. Я даже не знаю, правда ли это, просто догадываюсь

user12211554 13.10.2019 03:51
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
508
5
297 294
9
Перейти к ответу Данный вопрос помечен как решенный

Ответы 9

Это препроцессорный способ объявления интерфейсов. Вы помещаете интерфейс (объявления методов) в файл заголовка, а реализацию - в файл cpp. Приложениям, использующим вашу библиотеку, необходимо знать только интерфейс, к которому они могут получить доступ через #include.

Поскольку в C++ окончательный исполняемый код не несет никакой символьной информации, это более или менее чистый машинный код.

Таким образом, вам нужен способ описания интерфейса фрагмента кода, который отделен от самого кода. Это описание находится в заголовочном файле.

Ответ принят как подходящий

Ну, основная причина была бы в том, чтобы отделить интерфейс от реализации. Заголовок объявляет, «что» будет делать класс (или что-то еще, что реализуется), в то время как файл cpp определяет, «как» он будет выполнять эти функции.

Это уменьшает зависимости, так что код, использующий заголовок, не обязательно должен знать все детали реализации и любых других классов / заголовков, необходимых только для этого. Это сократит время компиляции, а также количество перекомпиляций, необходимых при изменении чего-либо в реализации.

Это не идеально, и вы обычно прибегаете к таким методам, как Pimpl Idiom, чтобы должным образом разделить интерфейс и реализацию, но это хорошее начало.

Не совсем так. Заголовок по-прежнему содержит основную часть реализации. С каких это пор переменные частного экземпляра стали частью интерфейса класса? Частные функции-члены? Тогда что, черт возьми, они делают в общедоступном заголовке? А с шаблонами он еще больше разваливается.

jalf 02.12.2008 21:13

Вот почему я сказал, что это не идеально, и идиома Pimpl нужна для большего разделения. Шаблоны - это совершенно другая баня червей - даже если бы ключевое слово "exports" полностью поддерживалось в большинстве компиляторов, это все равно было бы синтаксическим сахаром, а не реальным разделением.

Joris Timmermans 03.12.2008 11:17

Как с этим справляются другие языки? например - Ява? В Java нет концепции файла заголовка.

Lazer 13.06.2010 16:18

В Java более сильно поддерживается концепция «интерфейсов». На мой взгляд, эти интерфейсы являются дальнейшим развитием первоначальной идеи «заголовочного» файла, содержащего Только «что».

Joris Timmermans 14.06.2010 11:21

@Lazer: Java проще разбирать. Компилятор Java может анализировать файл, не зная всех классов в других файлах, и проверять типы позже. В C++ многие конструкции неоднозначны без информации о типе, поэтому компилятору C++ требуется информация о ссылочных типах для анализа файла. Вот почему ему нужны заголовки.

Niki 28.07.2010 23:06

@nikie: При чем тут "легкость" парсинга? Если бы у Java была грамматика, которая была бы хотя бы такой же сложной, как C++, она все равно могла бы просто использовать java-файлы. В любом случае, что насчет C? C легко разбирается, но использует как заголовки, так и файлы c.

Thomas Eding 03.12.2011 01:18

@Lazer: Java не только проще в синтаксическом анализе, но и не имеет шаблонов (да, у него есть дженерики, но их даже нет в работе).

Deduplicator 18.08.2014 22:07

Нет ли возможности C++ поддерживать объединенную реализацию с интерфейсом? Подавляющее большинство кода, использующего C++, такого как доказательство концепции, тесты, простые проекты, не получают выгоды от разделения и только усложняют управление вашим кодом из-за перебоев между двумя файлами.

Dmitry 07.04.2016 16:27

Можем ли мы также добиться абстракции, отделяя файлы заголовков от файлов реализации? означает, что мы хотим предоставить наш код третьей стороне, тогда мы также можем отправлять только файлы заголовков и скрывать нашу бизнес-логику.

Rohit Hajare 17.07.2017 08:22

@jalf они по-прежнему являются частью интерфейса, потому что класс определяет конкретную структуру модели памяти размером определенный. И виртуальное наследование там, где это имеет значение. C++ получил модель памяти на основе хранилища, а не на основе типов, как виртуальная машина Java. Java не выполняется ни на каком реальном оборудовании, поэтому она стала более абстрактной.

Swift - Friday Pie 20.12.2020 23:04

Потому что люди, разработавшие формат библиотеки, не хотели «тратить» место на редко используемую информацию, такую ​​как макросы препроцессора C и объявления функций.

Поскольку вам нужна эта информация, чтобы сообщить вашему компилятору, что «эта функция будет доступна позже, когда компоновщик будет выполнять свою работу», им пришлось придумать второй файл, в котором эта общая информация могла бы храниться.

Большинство языков после C / C++ хранят эту информацию в выходных данных (например, байт-код Java) или вообще не используют предварительно скомпилированный формат, всегда распространяются в исходной форме и компилируются на лету (Python, Perl).

Не получится, циклические ссылки. То есть вы не можете построить a.lib из a.cpp до сборки b.lib из b.cpp, но вы также не можете построить b.lib перед a.lib.

MSalters 03.12.2008 17:45

Java решила это, Python может это сделать, любой современный язык может это сделать. Но в то время, когда был изобретен C, ОЗУ была настолько дорогой и дефицитной, что это просто не было вариантом.

Aaron Digulla 03.12.2008 17:54

Часто вам может потребоваться определение интерфейса без необходимости отправлять весь код. Например, если у вас есть общая библиотека, вы должны отправить с ней файл заголовка, который определяет все функции и символы, используемые в общей библиотеке. Без файлов заголовков вам нужно будет отправить исходный код.

В рамках одного проекта файлы заголовков используются, IMHO, как минимум для двух целей:

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

Почему поставщики библиотек не могли просто отправить сгенерированный «заголовочный» файл? "Заголовочный" файл без препроцессора должен давать гораздо лучшую производительность (если только реализация не была действительно нарушена).

Tom Hawtin - tackline 02.12.2008 16:50

Я думаю, это не имеет значения, если файл заголовка сгенерирован или написан вручную, вопрос был не в том, «почему люди сами пишут файлы заголовков?», А в том, «почему у нас есть файлы заголовков». То же самое касается заголовков, свободных от препроцессора. Конечно, это было бы быстрее.

user21037 02.12.2008 16:53

Компиляция C++

Компиляция на C++ выполняется в 2 основных этапа:

  1. Первый - это компиляция «исходных» текстовых файлов в двоичные «объектные» файлы: файл CPP - это скомпилированный файл, который компилируется без каких-либо сведений о других файлах CPP (или даже библиотеках), если только он не передается через необработанное объявление или включение заголовка. Файл CPP обычно компилируется в "объектный" файл .OBJ или .O.

  2. Второй - это соединение всех «объектных» файлов и, таким образом, создание финального двоичного файла (библиотеки или исполняемого файла).

Какое место в этом процессе занимает ГЭС?

Плохой одинокий файл CPP ...

Компиляция каждого файла CPP не зависит от всех других файлов CPP, что означает, что если A.CPP требуется символ, определенный в B.CPP, например:

// A.CPP
void doSomething()
{
   doSomethingElse(); // Defined in B.CPP
}

// B.CPP
void doSomethingElse()
{
   // Etc.
}

Он не будет компилироваться, потому что A.CPP не может знать, что "doSomethingElse" существует ... Если в A.CPP нет объявления, например:

// A.CPP
void doSomethingElse() ; // From B.CPP

void doSomething()
{
   doSomethingElse() ; // Defined in B.CPP
}

Затем, если у вас есть C.CPP, который использует тот же символ, вы затем копируете / вставляете объявление ...

КОПИРОВАТЬ / ВСТАВИТЬ ПРЕДУПРЕЖДЕНИЕ!

Да, есть проблема. Копирование / вставка опасны, и их сложно поддерживать. Это означает, что было бы круто, если бы у нас был способ НЕ копировать / вставлять, но при этом объявлять символ ... Как мы можем это сделать? Путем включения некоторого текстового файла, который обычно имеет суффикс .h, .hxx, .h ++ или, как я предпочитаю для файлов C++, .hpp:

// B.HPP (here, we decided to declare every symbol defined in B.CPP)
void doSomethingElse() ;

// A.CPP
#include "B.HPP"

void doSomething()
{
   doSomethingElse() ; // Defined in B.CPP
}

// B.CPP
#include "B.HPP"

void doSomethingElse()
{
   // Etc.
}

// C.CPP
#include "B.HPP"

void doSomethingAgain()
{
   doSomethingElse() ; // Defined in B.CPP
}

Как работает include?

По сути, включение файла будет анализировать, а затем копировать и вставлять его содержимое в файл CPP.

Например, в следующем коде с заголовком A.HPP:

// A.HPP
void someFunction();
void someOtherFunction();

... источник B.CPP:

// B.CPP
#include "A.HPP"

void doSomething()
{
   // Etc.
}

... станет после включения:

// B.CPP
void someFunction();
void someOtherFunction();

void doSomething()
{
   // Etc.
}

Одна маленькая вещь - зачем включать B.HPP в B.CPP?

В текущем случае в этом нет необходимости, и B.HPP имеет объявление функции doSomethingElse, а B.CPP имеет определение функции doSomethingElse (которое само по себе является объявлением). Но в более общем случае, когда B.HPP используется для объявлений (и встроенного кода), не может быть соответствующего определения (например, перечислений, простых структур и т. д.), Поэтому включение может потребоваться, если B.CPP использует это объявление от B.HPP. В общем, "хороший вкус" для источника включать по умолчанию свой заголовок.

Заключение

Таким образом, файл заголовка необходим, потому что компилятор C++ не может искать только объявления символов, и поэтому вы должны помочь ему, включив эти объявления.

Последнее слово: вы должны поставить защиту заголовков вокруг содержимого ваших файлов HPP, чтобы быть уверенным, что несколько включений ничего не сломают, но в целом, я считаю, что основная причина существования файлов HPP объяснена выше.

#ifndef B_HPP_
#define B_HPP_

// The declarations in the B.hpp file

#endif // B_HPP_

или даже попроще (хотя и не стандартно)

#pragma once

// The declarations in the B.hpp file

Вам все равно нужно скопировать и вставить подпись из файла заголовка в файл cpp, не так ли?

nimcap 17.06.2012 21:17

@nimcap: You still have to copy paste the signature from header file to cpp file, don't you?: Нет необходимости. Пока CPP "включает" HPP, прекомпилятор автоматически копирует и вставляет содержимое файла HPP в файл CPP. Я обновил ответ, чтобы прояснить это.

paercebal 18.06.2012 12:47

Спасибо, ваша идея копирования / вставки оказалась полезной. Но ваша точка зрения «Он не будет компилироваться, потому что A.cpp не знает, что« doSomethingElse »существует», мне кажется неправильным. При компиляции A.cpp компилятор знает типы аргументов и возвращаемое значение doSomethingElse из самого вызова; он может предполагать, что doSomethingElse определен в другом модуле и полагаться на компоновщик для заполнения зависимости (или возвращать ошибку, если он не может найти ее определение или типы аргументов / возвращаемого значения несовместимы в A.cpp и B.cpp). Я до сих пор не понимаю необходимости заголовков. Кажется, это просто довольно уродливый произвольный дизайн.

Boris Burkov 06.01.2013 08:52

@ Боб: While compiling A.cpp, compiler knows the types of arguments and return value of doSomethingElse from the call itself. Нет, это не так. Он знает только типы, предоставленные пользователем, который в половине случаев даже не потрудится прочитать возвращаемое значение. Затем происходят неявные преобразования. А когда у вас есть код: foo(bar), вы даже не можете быть уверены, что foo - это функция. Таким образом, компилятор должен иметь доступ к информации в заголовках, чтобы решить, правильно ли компилируется исходный код или нет ... Затем, как только код будет скомпилирован, компоновщик просто свяжет вместе вызовы функций.

paercebal 07.01.2013 12:48

@Bob: [продолжение] ... Теперь, я думаю, компоновщик мог бы выполнять работу, проделанную компилятором, что тогда сделало бы ваш вариант возможным. (Я предполагаю, что это предмет предложения о «модулях» для следующего стандарта). Seems, they're just a pretty ugly arbitrary design.: Если бы C++ действительно был создан в 2012 году. Но помните, что C++ был построен на C в 1980-х годах, и в то время ограничения были совершенно другими (IIRC, в целях принятия было решено сохранить те же компоновщики, что и C).

paercebal 07.01.2013 12:53

@paercebal Спасибо за объяснения и примечания, paercebal! Почему я не могу быть уверен, что foo(bar) - это функция, если она получена как указатель? На самом деле, говоря о плохом дизайне, я виню C, а не C++. Мне действительно не нравятся некоторые ограничения чистого C, такие как наличие файлов заголовков или наличие функций, возвращающих одно и только одно значение, при одновременном приеме нескольких аргументов на входе (не кажется ли естественным, что ввод и вывод ведут себя аналогичным образом ; почему несколько аргументов, а один выход?) :)

Boris Burkov 08.01.2013 21:20

@Bobo: Why can't I be sure, that foo(bar) is a function: foo может быть типом, поэтому у вас должен быть вызван конструктор класса. In fact, speaking of bad design, I blame C, not C++: Я могу винить C во многих вещах, но то, что он был разработан в 70-х, не будет одним из них. Опять же, ограничения того времени ... such as having header files or having functions return one and only one value: Кортежи могут помочь смягчить это, а также передача аргументов по ссылке. Теперь, каков будет синтаксис для получения нескольких возвращаемых значений и стоит ли менять язык?

paercebal 09.01.2013 14:23

Ответ должен был быть выбран в качестве ответа на этот пост. Оригинал тоже хорош, но этот ответ охватывает в основном все.

Tarik 20.09.2016 19:58

Почему нельзя было просто включить B.CPP в A.CPP?

BozanicJosip 31.08.2017 00:00

Ссылка @Lazer wayback machine на "Организация файлов кода на C и C++" web.archive.org/web/20100124195524/http://www.gamedev.net/… / текущая ссылка: gamedev.net/articles/programming/…

qubodup 22.07.2018 20:27

Поскольку C, где зародилась эта концепция, существует 30 лет, и тогда это был единственный жизнеспособный способ связать код из нескольких файлов.

Сегодня это ужасный взлом, который полностью уничтожает время компиляции в C++, вызывает бесчисленные ненужные зависимости (потому что определения классов в файле заголовка предоставляют слишком много информации о реализации) и так далее.

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

Mateen Ulhaq 24.05.2019 11:56

он предшествовал K&R C. Почти все языки до этого использовали те же парадигмы, исключением были бы языки, подобные Паскалю, у которых был специальный модуль компиляции, называемый «unit», который был и заголовком, и реализацией в одном и основном, называемом «программа». Все дело в том, чтобы разделить программу на части кода, которыми может управлять компилятор, и сократить время компиляции \ разрешить инкрементную компиляцию.

Swift - Friday Pie 20.12.2020 23:13

Потому что C++ унаследовал их от C. К сожалению.

Почему наследование C++ от C неудачно?

Lokesh 02.06.2017 08:16

@Lokesh Из-за своего багажа :(

陳 力 03.06.2018 09:35

Как это может быть ответом?

Shuvo Sarker 27.08.2018 22:24

@ShuvoSarker, потому что, как показали тысячи языков, нет технического объяснения того, как C++ заставляет программистов писать сигнатуры функций дважды. Ответ на вопрос "почему?" это «история».

Boris 12.02.2019 20:16

@Boris забавно, что C на самом деле не требовал писать их дважды. ANd C изначально вообще не нуждался в прототипах, потому что он работал на платформах, допускающих такую ​​реализацию. У них даже не было регистров стека, «стек» был просто областью памяти, управляемой созданным кодом. Это C++, и современные платформы перешли на регистровый или смешанный способ вызова функций, поэтому требуется отдельный прототип, если мы скрываем реализацию и можем ли мы перегрузить. Довольно много классических (Фортран, Паскаль) и современных языков тоже. Отсутствие такового обычно является подписью переводчика.

Swift - Friday Pie 20.12.2020 23:18

Отвечая на Ответ MadKeithV,

This reduces dependencies so that code that uses the header doesn't necessarily need to know all the details of the implementation and any other classes/headers needed only for that. This will reduce compilation times, and also the amount of recompilation needed when something in the implementation changes.

Другая причина в том, что заголовок дает каждому классу уникальный идентификатор.

Итак, если у нас есть что-то вроде

class A {..};
class B : public A {...};

class C {
    include A.cpp;
    include B.cpp;
    .....
};

У нас будут ошибки, когда мы попытаемся построить проект, поскольку A является частью B, с заголовками мы бы избежали такой головной боли ...

Это конкретно называется абстракцией, я прав?

50_Seconds _Of_Coding 28.10.2020 06:54

Другие вопросы по теме

Внешние зависимости / Заголовочные файлы
Плюсы и минусы размещения всего кода в файлах заголовков на C++?
Авто определяет в редакторах Си ... Почему?
В чем разница между #include <filename> и #include "filename"?
Дополнительная информация из инструкций по принципам и практике программирования 3ed неясна
Почему разделение интерфейса и реализация специализации `std::formatter` приводят к сбою ограничений?
Почему объявление функции глобальной области со встроенным типом arg должно быть видимым перед неквалифицированным вызовом этого имени с аргументом типа шаблона?
Почему я должен указывать пространство имен глобальной функции при вызове ее из функции-члена с таким же именем в том же пространстве имен?
Сборка Busybox завершается неудачно: заголовок ncurses не найден в Archlinux (спойлер: у меня уже есть пакет ncurses)
Какие ключевые слова из заголовка попадают в файл cpp?