2017-02-09 2 views
75

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

Вопрос: почему программисты C не пропускают все входящие и просто не конкатенируют свои исходные файлы C, а затем компилируют его? Если вы разместите все свои приложения в одном месте, вам нужно будет только определить, что вам нужно, а не во всех исходных файлах.

Вот пример того, что я описываю. Здесь у меня есть три файла:

// includes.c 
#include <stdio.h> 
// main.c 
int main() { 
    foo(); 
    printf("world\n"); 
    return 0; 
} 
// foo.c 
void foo() { 
    printf("Hello "); 
} 

Делая что-то вроде cat *.c > to_compile.c && gcc -o myprogram to_compile.c в моей Makefile я могу уменьшить количество кода, я пишу.

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

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

+20

Ваш пример неправильный: прототип необходим для 'foo'. – LPs

+11

Читайте о передовых декларациях, и вы поймете, что не так, имея только один .c файл. А также это будет сумасшедший беспорядок. –

+1

C - это не один из современных языков, где символы могут быть определены только * где-то * в контексте проекта. Он имеет очень строгие правила в отношении областей видимости и разрешения. Используйте другой язык, если вы хотите область проекта для именованных идентификаторов. – grek40

ответ

105

Некоторые программы построены таким образом.

Типичный пример: SQLite. Он иногда компилируется как amalgamation (выполняется во время сборки из многих исходных файлов).

Но этот подход имеет свои плюсы и минусы.

Очевидно, что время компиляции увеличится довольно много. Так что это практично, только если вы собираете этот материал редко.

Возможно, компилятор может немного оптимизировать. Но с оптимизацией времени соединения (например, при использовании последнего GCC, компиляции и связи с gcc -flto -O2) вы можете получить тот же эффект (конечно, за счет увеличения времени сборки).

Я не должен написать файл заголовка для каждой функции

Это неправильный подход (чтобы иметь один файл заголовка каждой функции).Для проекта с одним человеком (менее ста тысяч строк кода, например, KLOC = кило-линия code), вполне разумно - по крайней мере, для небольших проектов - иметь общий заголовочный файл (4415) pre-compile при использовании GCC), в котором будут содержаться декларации всех публичных функций и типов и, возможно, определения из static inline функций (достаточно маленьких и достаточно часто вызываемых, чтобы получить прибыль от inlining). Например, sash shell организован таким образом (а также lout formatter, с 52   KLOC).

Возможно, у вас также есть несколько файлов заголовков и, возможно, есть отдельный заголовок «группировки», который все они (и которые вы можете предварительно скомпилировать). Смотрите, например jansson (который на самом деле имеет один общественного заголовка файла) и GTK (который имеет много внутренних заголовков, но большинство приложений, использующих его have только один #include <gtk/gtk.h>, который, в свою очередь, включают все внутренние заголовки). На противоположной стороне POSIX имеет большое количество файлов заголовков, и он документирует, какие из них должны быть включены и в каком порядке.

Некоторые люди предпочитают иметь много файлов заголовков (а некоторые даже предпочитают помещать объявление одной функции в свой собственный заголовок). Я не делаю (для личных проектов или небольших проектов, на которых только два или три человека совершают кодекс), но это вопрос вкус. Кстати, когда проект много растет, часто случается, что набор файлов заголовков (и единиц перевода) значительно меняется. Посмотрите также на REDIS (у него есть 139 .h файлы заголовков и 21 файлов, т. Е. Единицы перевода, суммирующие 126   KLOC).

Наличие одного или нескольких translation units также является вопросом вкуса (и удобства и привычек и условностей). Мое предпочтение состоит в том, чтобы иметь исходные файлы (то есть единицы перевода), которые не слишком малы, обычно несколько тысяч строк каждый, и часто имеют (для небольшого проекта менее 60   KLOC) общий одиночный заголовочный файл. Не забудьте использовать какой-нибудь инструмент build automation, например GNU make (часто с parallel, через make -j, тогда у вас будет несколько процессов компиляции, выполняющихся одновременно). Преимущество такой организации исходного файла заключается в том, что компиляция достаточно быстро. BTW, в некоторых случаях стоит подход metaprogramming: некоторые из ваших (внутренних заголовков или единиц перевода) C «исходные» файлы могут быть сгенерированы чем-то другим (например, какой-то скрипт в AWK, некоторые специализированные программы на C, такие как bison или ваш собственная вещь).

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

я настоятельно рекомендую изучить исходный код и создать некоторые существующихfree software проектов (например, те, на GitHub или SourceForge или ваш любимом дистрибутиве Linux). Вы узнаете, что они разные подходы.Помните, что в C конвенции и привычки материи много на практике, так есть различные способов организовать свой проект в .c и .h файлов. Читайте о C preprocessor.

Это также означает, что я не должен включать стандартные библиотеки в каждом файле я создаю

Включаешь заголовочные файлы, а не библиотеки (но вы должны link библиотеки). Но вы можете включить их в каждый файл .c (и это делают многие проекты), или вы можете включить их в один заголовок и предварительно скомпилировать этот заголовок, или у вас может быть дюжина заголовков и включать их после заголовков системы в каждом сбор единица измерения. YMMV. Обратите внимание, что на современных компьютерах быстро выполняется предварительная обработка (по крайней мере, когда вы просите компилятор оптимизировать, поскольку оптимизация занимает больше времени, чем обработка & предварительной обработки).

Обратите внимание, что в какой-то #include -d файл находится обычный (и не определяется спецификацией C). Некоторые программы имеют некоторый код в каком-то таком файле (который затем не следует называть «заголовком», просто «включенным файлом», и который тогда не должен иметь суффикса .h, но что-то вроде .inc). Посмотрите пример на XPM. С другой стороны, вы, возможно, в принципе не имеете никаких собственных файлов заголовков (вам все равно нужны файлы заголовков из реализации, например <stdio.h> или <dlfcn.h> из вашей системы POSIX), а также скопируйте и вставьте дублированный код в файлы .c -e.g. имеют строку int foo(void); в каждом файле .c, но это очень плохая практика и нахмурился. Тем не менее, некоторые программы - это , генерирующие файлы C, в которых используется общий контент.

BTW, C или C++ 14 не имеют модулей (например, OCaml). Другими словами, в модуле C в основном используется соглашение .

(обратите внимание, что наличие многих тысяч очень маленьких.h и .c файлов всего лишь нескольких десятков строк каждый может замедлить время сборки драматический, имея сотню файлов в нескольких сот линий, каждый является более разумным, в срок строительства.)

Если вы начнете работать над проектом с одним человеком в C, я бы предложил сначала иметь один заголовочный файл (и предварительно скомпилировать его) и несколько блоков перевода .c. На практике вы измените файлы .c гораздо чаще, чем .h. Если у вас более 10 KLOC, вы можете реорганизовать это в несколько файлов заголовков. Такой рефакторинг сложный для проектирования, но его легко сделать (просто копия &, вставляющая кусок кодов). У других людей были бы разные предложения и подсказки (и это нормально!). Но не забудьте включить все предупреждения и отладочную информацию при компиляции (поэтому компилируйте с помощью gcc -Wall -g, возможно, установив CFLAGS= -Wall -g в свой Makefile). Используйте отладчик gdbvalgrind ...). Попросите оптимизацию (-O2), когда вы сравниваете уже отлаженную программу. Также используйте систему управления версиями, например Git.

Напротив, если вы проектируете более крупный проект, на котором будет работать , будет работать несколько человек, было бы лучше иметь несколько файлов - даже несколько файлов заголовков - (интуитивно, каждый файл имеет одного человека, несущего основную ответственность за он, а другие делают незначительный вклад в этот файл).

В комментарии, добавьте:

Я говорю о написании моего кода в большом количестве различных файлов, но с помощью Makefile, чтобы сцепить их

Я не вижу, почему это было бы полезно (за исключением очень странных случаев). Гораздо лучше (и очень обычная и обычная практика) собрать каждую блок перевода (например, каждый файл .c) в его object file (файл .oELF на Linux) и link их позже. Это легко с make (на практике, когда вы измените только один файл .c, например, чтобы исправить ошибку, только этот файл компилируется, а инкрементная сборка очень быстрая), и вы можете попросить его скомпилировать объектные файлы в parallel, используя make -j (а затем ваша сборка идет очень быстро на вашем многоядерном процессоре).

+1

еще один пример - это так называемый [Unity Build] (http://stackoverflow.com/questions/847974/the-benefits-disadvantages-of-unity-builds), используемый в C++, но я думаю, что он применим и к C. Основная причина заключается в ускорении строительства за счет снижения перегрева IO. –

+13

Наличие одного файла заголовка в любом проекте - ужасная идея. –

+7

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

9

Основная причина - время компиляции. Компиляция одного небольшого файла при его изменении может занять короткое время. Если бы вы скомпилировали весь проект всякий раз, когда вы меняете одну строку, тогда вы собираете, например, 10 000 файлов каждый раз, что может занять много времени.

Если у вас есть - как в приведенном выше примере - 10000 исходных файлов и компиляции один занимает 10   мс, то весь проект строится постепенно (после изменения одного файла) или в (10   мс + привязка времени) при компиляции просто этот измененный файл, или (10   ms * 10000 + короткое время связывания), если вы скомпилируете все как единый конкатенированный кадр.

28

Вы мог сделать это, но мы хотели бы выделить программы C в отдельные переводе единиц, главным образом потому, что:

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

  2. Стандартная библиотека C состоит из предварительно скомпилированных компонентов. Вы действительно хотите перекомпилировать все это?

  3. С другой стороны, легче работать с другими программистами, если база кода разделена на разные файлы.

+2

Я никогда не слышал о единицах перевода раньше. Спасибо, я пойду и узнаю о них. Какие-нибудь хорошие уроки с головы? – OhFiddyYouSoWiddy

+5

1) не всегда верно, особенно для C++; Я видел значительное сокращение времени сборки от конкатенации. «единица перевода» - это не то, что требует учебника, это всего лишь способ сказать «файл C + все его включенные файлы». Это фраза, которая часто используется в стандарте C - определения до конца единицы перевода. – pjc50

+3

@ pjc50: C++, с его шаблонами, возможностями оценки времени компиляции и перегрузкой функций - совсем другой зверь. (Для C++ я использую распределенную среду сборки, но все еще трачу на этом сайте слишком много времени во время компиляции.) – Bathsheba

18
  • С модульности, вы можете поделиться своей библиотекой, не разделяя код.
  • Для больших проектов, если вы измените один файл, вы в итоге получите , составляя полный проект.
  • У вас может быть больше проблем с памятью при попытке скомпилировать большие проекты.
  • У вас могут быть круговые зависимости в модулях, модульность помогает в их обслуживании.

В вашем подходе может быть определенная прибыль, но для таких языков, как C, компиляция каждого модуля имеет больше смысла.

3

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

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

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

Однако его можно использовать для разбиения единицы перевода на более мелкие биты, которые совместно используют доступ к функциям таким образом, который напоминает доступность пакета Java.

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

Например, вы можете разбить единицы перевода на два файла:

// a.c 

static void utility() { 
} 

static void a_func() { 
    utility(); 
} 

// b.c 

static void b_func() { 
    utility(); 
} 

Теперь вы добавили файл для ЕП:

// ab.c 

static void utility(); 

#include "a.c" 
#include "b.c" 

И ваша система сборки не строит либо a.c или b.c, но вместо этого строит только ab.o из ab.c.

Что делает ab.c?

Он включает в себя оба файла для создания единой единицы перевода и предоставляет прототип утилиты. Так что код в a.c и b.c мог видеть его, независимо от того, в каком порядке они включены, и не требуя, чтобы функция была extern.

16

Потому что расщепление вещей - хороший дизайн программы. Хороший дизайн программы - это модульность, автономные модули кода и повторное использование кода. Как выясняется, здравый смысл заставит вас очень далеко при разработке программ: вещи, которые не принадлежат друг другу, не должны размещаться вместе.

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

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

Предположим, вы пишете прошивку для управления автомобилем. Один модуль в программе управляет FM-радиоприемником. Затем вы повторно используете радиокод в другом проекте, чтобы управлять FM-радио на смартфоне. И тогда ваш радиокод не будет компилироваться, потому что он не может найти тормоза, колеса, шестерни и т. Д. Вещи, которые не имеют ни малейшего смысла для FM-радио, не говоря уже о смартфоне, о котором нужно знать.

Что еще хуже, если у вас плотная связь, ошибки повторяются во всей программе, а не остаются локальными в модуле, где находится ошибка. Это делает последствия ошибок более серьезными. Вы пишете ошибку в коде FM-радио, а затем внезапно тормоз автомобиля перестает работать. Даже если вы не коснулись кода тормоза с обновлением, содержащим ошибку.

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

8

Хотя вы все еще можете написать свою программу модульным способом и построить ее как единую единицу перевода, вы пропустите все , механизмы C обеспечивают принудительное выполнение этой модульности. С несколькими единицами трансляции вы обладаете точным контролем на интерфейсах модулей, используя, например, extern и static ключевые слова.

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

+5

В C, ** модульность ** в основном ** обычная ** (и не связана с организацией в файлах). –

+0

Истина (обратите внимание, что я упоминаю * единицы перевода *, а не файлы), но все же существуют механизмы, обеспечивающие ** модуляцию ** модульности. –

5

Если вы разместите все свои объекты в одном месте, вам нужно будет только определить, что вам нужно, а не во всех исходных файлах.

Это цель .h, поэтому вы можете определить, что вам нужно, и включить его повсюду. В некоторых проектах есть заголовок everything.h, который включает в себя каждый файл .h. Таким образом, ваш pro может быть создан с отдельными .c файлами.

Это означает, что я не должен написать файл заголовка для каждой функции я создаю [...]

Вы не должны писать один файл заголовка для каждой функции в любом случае. У вас должен быть один заголовочный файл для набора связанных функций. Таким образом, ваш con также недействителен.

12

Файлы заголовков должны определять интерфейсы - это желательное соглашение. Они не предназначены для объявления всего, что находится в соответствующем файле .c, или группе из файлов .c. Вместо этого они объявляют все функциональные возможности в файлах .c, которые доступны для их пользователей. Хорошо продуманный файл .h содержит базовый документ интерфейса, открытый кодом в файле .c, даже если в нем нет ни одного комментария. Один из способов приблизиться к дизайну модуля C - сначала записать файл заголовка, а затем реализовать его в одном или более файлах .c.

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

15

Вашего подход конкатенация .c файлов полностью нарушен:

  • Даже если команда cat *.c > to_compile.c поставят все функции в одном файл, вопросов заказа: Вы должны иметь каждую функцию объявлены перед первым использованием ,

    То есть у вас есть зависимости между вашими .c-файлами, которые вынуждают определенный порядок. Если ваша команда конкатенации не соблюдает этот порядок, вы не сможете скомпилировать результат.

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

  • При объединении всего в один файл, вы принудительно завершаете перестройку всякий раз, когда изменяется одна строка в вашем проекте.

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

  • Вы теряете возможность параллельной компиляции, когда вы объединяете все в один файл.

    У вас есть большой жирный 12-ядерный процессор с поддержкой гиперпоточности? Жаль, ваш объединенный исходный файл скомпилирован одним потоком. Вы просто потеряли ускорение с коэффициентом больше 20 ... Хорошо, это крайний пример, но у меня уже есть программное обеспечение с make -j16, и, говорю вам, это может иметь огромное значение.

  • Время компиляции, как правило, не линейное.

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

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

Не ошибитесь: Даже если речь идет со всеми этими проблемами, есть люди, которые используют .c файл конкатенации на практике, и некоторые C++ программисты получают довольно много в ту же точку, перемещая все в шаблоны (так что реализация найдена в файле .hpp и нет связанного файла .cpp), позволяя препроцессору выполнять конкатенацию. Я не вижу, как они могут игнорировать эти проблемы, но они это делают.

Также обратите внимание, что многие из этих проблем проявляются только при больших размерах проекта. Если ваш проект составляет менее 5000 строк кода, все равно относительно неважно, как вы его скомпилируете. Но когда у вас более 50000 строк кода, вам определенно нужна система сборки, которая поддерживает инкрементные и параллельные сборки. В противном случае вы тратите свое рабочее время.

+0

Btw, анекдот. Один из моих больших долгосрочных проектов, C++, скомпилированный с компилятором Borlands, имеет значительное количество сгенерированных исходных файлов, содержащих тысячи строк вызовов функций. Некоторая странная причуда их компилятора заставила время компиляции увеличивать экспоненциально по количеству вызовов, до того момента, когда 5000-строчный файл будет занимать порядка минут. Я все еще не могу это объяснить, но восстановление проекта было кошмаром. В конечном итоге я переключился на генерацию массивов данных и их циклизацию, что значительно уменьшило время компиляции от минут до миллисекунд, но ... –

+1

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

Смежные вопросы