Как правильно подключать файлы c
В рамках серьезных проектов на языке C может использоваться множество заголовочных файлов, имеющих расширение .h. В этих файлах обычно размещаются прототипы функций и макросы, которые используются более чем в одном файле исходного кода.
Заголовочные файлы подключаются к файлам исходного кода приложения по мере необходимости с помощью директивы #include.
Данная директива отличается от ранее рассмотренных директив для подключения системных заголовочных файлов тем, что вместо скобок используются кавычки.
Также следует помнить о том, что содержимое каждого заголовочного файла должно быть защищено от рекурсивного подключения. Для этой цели используются три директивы препроцессора.
#ifndef SUMS_H
#define SUMS_H
…
#endif /*SUMS_H*/
В некоторых файлах подключение заголовочного файла должно осуществляться на основе какого-либо критерия. Например, первый заголовочный файл может использоваться для одной архитектуры центрального процессора, а второй — для другой. Эта задача также решается с помощью препроцессора.
#if x86_64
#include «system64.h»
#elif x86
#include «system32.h»
#endif
Пример
В данном простом примере функция вывода приветствия находится в отдельном файле исходного кода (hello.c), который подключается к основному файлу исходного кода программы (hellomain.c) с помощью заголовочного файла (hello.h).
Это содержимое файла исходного кода hello.c:
Как правильно подключать файлы c
Если программа содержит много кода, то более оптимально было бы разнести отдельные части кода по отдельным файлам. Например, одни функции могут храниться в одном файле исходного кода, другие функции — в другом файле.
Например, определим файл sum.cpp , который будет иметь следующий код:
int sum(int a, int b)
Это функция вычисления суммы чисел.
Добавим еще один файл — , который будет содержать объявление функции sum:
int sum(int, int);
И также определим главный файл, который назовем app.cpp :
#include #include "sum.h" // подключаем файл sum.h int main() < int result < sum(5, 4)>; std::cout #include "sum.h"
Файл sum.h еще называется заголовочным файлом (header file), так как содержит объявление, заголовок функции. ПРичем в данном случае предполагается что все файлы располагаются в одном каталоге:
Можно было бы и не подключать файл sum.h и вообще не создавать его, а объявление функции поместить непосредственно в файл app.cpp. Но при изменении функции может потребоваться изменить и ее объявление. И если функция sum используется в нескольких файлах с исходным кодом, то в каждом из этих файлов придется менять ее объявление. В данном же случае достаточно изменить объявление функции в одном файле — sum.h.
При компиляции через g++ необходимо передать все файлы через пробел компилятору:
g++ app.cpp sum.cpp -o app
То же самое верно и для компиляции через Clang::
clang++ app.cpp sum.cpp -o app.exe
На выходе будет сгенерирован единый файл app.
При работе в Visual Studio заголовочные файлы обычны помещаются в каталог «Headers»:

А при компиляции все файлы автоматически компилируются в один.
Многофайловые программы
Запуск gcc позволяет обработать файл с исходным кодом препроцессором и далее скомпилировать его. Однако при этом сам инструмент gcc не компилирует файл исходного кода в конечный исполняемый файл. Он компилирует его в объектный файл, после чего вызывает так называемый линковщик, или компоновщик. Но зачем надо сначала получать объектный файл, а потом из него уже исполняемый? Для программ, состоящих из одного файла, такой необходимости нет. Хотя при желании здесь также можно отказаться от компоновки, если выполнить команду gcc с ключом -c:
gcc -c hello.c
В результате получится файл с расширением *.o. Чтобы получить из объектного файла исполняемый, надо использовать ключ -o:
gcc -o hello hello.o
Для программ, состоящих из нескольких файлов исходного кода, получение объектных файлов является необходимым. Именно из них потом компонуется единственный исполняемый файл.
Компиляция программы, состоящей из нескольких файлов исходного кода
Рассмотрим пример. Пусть в одном файле определена пара функций, а в другом, содержащем функцию main() , осуществляется их вызов.
#include void l2r(char **c, int n) { int i, j; for(i=0; in; i++, c++) { for (j=0; ji; j++) printf("\t"); printf ("%s\n",*c); } } void r2l(char **c, int n) { int j; for(; n>0; n--, c++) { for (j=1; jn; j++) printf("\t"); printf ("%s\n",*c); } }
#include #define N 5 int main () { char strs[N][10]; char *p[N]; int i; for(i=0; iN; i++) { scanf("%s", strs[i]); p[i] = &strs[i][0]; } l2r(p, N); r2l(p, N); }
В теле функции main() заполняется массив, состоящий из строк, а также массив указателей на эти строки. Далее в функции l2r() и r2l() передаются ссылки на первый элемент массива указателей и значение символической константы N. Эти функции осуществляют вывод элементов массива строк с отступами.
Чтобы получить исполняемый файл этой программы, надо сначала получить объектные файлы из исходных:
gcc -c superprint.c gcc -c main.c
Тоже самое можно сделать за один вызов gcc:
gcc -c superprint.c main.c
Или даже вот так, если в каталоге находятся только файлы текущего проекта:
gcc -c *.c
В любом случае в каталоге появятся два объектных файла: superprint.o и main.o. Далее их можно скомпилировать в один исполняемый файл так:
gcc -o myprint main.o superprint.o
gcc -o myprint *.o
Обратите внимание на то, что в данном случае обязательно требуется указывать имя исполняемого файла. Такой вариант недопустим:
gcc -o main.o superprint.o
Если теперь запустить файл myprint, то программа будет ожидать ввода пяти слов, после чего выведет их на экран два раза по-разному (с помощью функций l2r() и r2l() ):

Задумаемся, каким образом в представленной выше программе функция main() «узнает» о существовании функций l2r() и r2l() . Ведь в исходном коде файла main.c нигде не указано, что мы подключаем файл superprint.c, содержащий эти функции. Действительно, если попытаться получить из main.c отдельный исполняемый файл, т.е. скомпилировать программу без superprint.c:
gcc main.c
, то ничего не получиться. Компилятор сообщит об ошибке вызова неопределенных идентификаторов. Получить из файла superprint.c отдельный исполняемый файл вообще невозможно, т.к. там отсутствует функция main() . А вот получить из этих файлов отдельные объектные файлы можно. Представим, что одни объектные файлы как бы «выставляют наружу» имена определенных в них функций и глобальных переменных, а другие — вызовы этих имен из тел других функций. Дальше объектные файлы «ожидают», что имена будут связаны с их вызовами. Связывание происходит при компиляции исполняемого файла из объектных.
Создание заголовочных файлов
Продолжим разбирать приведенную выше программу. Что будет, если в функции main() осуществить неправильный вызов функций l2r() и r2l() ? Например, указать неверное количество параметров. В таком случае создание объектных файлов пройдет без ошибок, и скорее всего удастся получить исполняемый файл; но вот работать программа будет неправильно. Такое возможно потому, что ничего не контролирует соответствие вызовов прототипам (объявлениям) функций.
Куда правильней сообщать о неверном вызове функций уже при получении объектного файла. Поэтому хотя можно обойтись и без этого, но очень желательно сообщать функции main() прототипы функций, которые из нее вызываются. Это можно сделать, прописав объявления функций в файле main.c:
… void l2r (char **c, int n); void r2l (char **c, int n); main () …
Теперь, если мы передадим неправильные параметры, ошибка возникнет уже на этапе получения объектных файлов.
А теперь представим, что программа у нас несколько больше и содержит десяток файлов исходного кода. Файл aa.c требует функций из файла bb.c, dd.c, ee.c. В свою очередь dd.c вызывает функции из ee.c и ff.c, а эти два последних файла активно пользуются неким файлом stars.c и одной из функций в bb.c. Программист замучится сверять, что чего вызывает откуда и куда, где и какие объявления надо прописывать. Поэтому все прототипы (объявления) функций проекта, а также совместно используемые символические константы и макросы выносят в отдельный файл, который подключают к каждому файлу исходного кода. Такие файлы называются заголовочными; с ними мы уже не раз встречались. В отличие от заголовочных файлов стандартной библиотеки, заголовочные файлы, которые относятся только к вашему проекту, при подключении к файлу исходного кода заключаются в кавычки, а не скобки. Об этом упоминалось в предыдущем уроке.
Итак, более грамотно будет не добавлять объявления функций в файл main.c, а создать заголовочный файл, например, myprint.h и поместить туда прототипы функций l2r() и r2l() . А в файле main.c следует прописать директиву препроцессора:
#include "myprint.h"
В принципе смысла подключать myprint.h к файлу superprint.c в данном случае нет, т.к. последний не использует никаких сторонних функций, кроме стандартной библиотеки. Но если планируется расширять программу и есть вероятность, что в файле superprint.c будут вызываться сторонние для него функции, то будет надежней сразу подключить заголовочный файл.
Обратим внимание еще на один момент. Стоит ли в описанном в этом уроке примере выносить константу N в заголовочный файл? Здесь нельзя дать однозначный ответ. Если ее туда вынести, то она станет доступна в обоих файлах, и поэтому можно изменить прототипы функций так, чтобы они принимали только один параметр (указатель), а значение N будет известно функциям их заголовочного файла. Однако стоит ли так делать? В функции r2l() второй параметр изменяется в процессе ее выполнения, что делать с константой будет невозможно. Придется переписывать тело функции. Кроме того, вдруг в последствии нам захочется использовать файл superprint.c в другом проекте, где будут свои порядки, и константы N в заголовочном файле не найдется.
В таком случае лучше N не выносить в заголовочный файл. Хотя имейте в виду, в каких-то проектах символическая константа может использоваться так часто и во множестве функций, что ее будет лучше поместить в заголовочный файл.
Особенности использования глобальных переменных
Помните, если можно отказаться от использования глобальных переменных, то лучше это сделать. Желательно стремиться к тому, чтобы любой файл проекта, скажем так, «не лез к соседу за данными, а сосед не разбрасывал эти данные в виде глобальных переменных». Обмен данными между функциями должен осуществлять за счет передачи данных в качестве параметров и возврата значений с помощью оператора return . (Массивов это не касается.)
Однако в языке программирования C есть проблема. С помощью return можно вернуть только одно значение. Но могут быть случаи, когда функция должна изменить несколько переменных (здесь не имеются ввиду элементы массива). В таком случае без глобальных переменных обойтись сложно.
- Если в файле aa.c объявлена переменная за пределами любой функции (например, так: intcount ), то она является глобальной для всех файлов проекта. Чтобы получить значение этой переменной в файле aa.c достаточно просто указать ее имя (если в функции нет локальной переменной с тем же именем). Чтобы получить значение из других файлов, надо указать, что имеется в виду глобальная переменная, а не локальная. Делается это с помощью ключевого слова extern (например, extern count ).
- Бывают ситуации, когда в одном файле для нескольких содержащихся в нем функций нужна глобальная переменная. Но эта переменная не должна быть доступна функциям, содержащимся в других файлах. В таком случае глобальная переменная объявляется с ключевым словом static (например, static int count ). Тем самым мы как бы скрываем глобальную переменную.
Напишите простые наглядные примеры, использования глобальных функций:
- Объявите глобальную переменную в одном файле, а получите ее значение в другом файле (выведите на экран).
- Объявите в одном файле статическую глобальную переменную. Выведите ее значение на экран из функции данного файла. Попытайтесь сделать это из функции другого файла.
- Создайте две глобальные переменные в одном файле. В другом файле напишите функцию, которая меняет их значение.
Курс с решением части задач:
pdf-версия
Как правильно подключать файлы c
Подключаемый файл это файл, содержащий определения функций и переменных, а также макроопределения вместе с некоторыми исходными файлами. Для использования в программе подключаемых файлов применяется директива препроцессора ‘#include’.
- Системные подключаемые файлы используются для определения интерфейсов к составляющим операционной системы. Они подключаются для предоставления объявлений и определений, требуемых для работы с системными вызовами и библиотеками.
- Подключаемые файлы пользователя содержат определения для интерфейсов между исходными файлами программы.
Обычно подключаемые файлы заканчиваются на ‘.h’ и следует избегать использования других стандартов.
Как файлы пользователя, так и системные файлы включаются в программу с использованием директивы препроцессора ‘#include’. Она имеет три модификации:
'#include '
Эта модификация используется для подключения системных файлов. При ее выполнении производится поиск файла с именем FILE в списке указанных заранее каталогов, а затем в стандартном списке системных каталогов. С помощью опции ‘-I’ указываются каталоги для поиска подключаемых файлов. Опция ‘-nostdinc’ запрещает поиск в стандартных системных каталогах и производит поиск только в указанных каталогах.
Синтаксис такой модификации директивы ‘#include’ довольно специфичен, потому как комментарии внутри ‘<. >‘ не распознаются. Поэтому в строке ‘#include *y>’ последовательность символов ‘*’ не начинает комментарий, а указанная директива включает в программу файл с именем ‘x/*y’.
Аргумент FILE не может содержать символа ‘>’, хотя он может содержать символ ‘
'#include "FILE"'
Эта модификация применяется для подключаемых файлов для программ пользователя. Сначала файл FILE просматривается в текущем каталоге, а затем в каталогах для системных подключаемых файлов. Текущим каталогом является каталог текущего обрабатываемого файла. Он просматривается в первую очередь, так как предполагается, что в нем находятся файлы, имеющие отношение к текущему обрабатываемому файлу. (Если указана опция ‘-I-‘, то текущий каталог не просматривается.)
Аргумент FILE не может содержать символов ‘»‘. Символы backslash интерпретируются как отдельные символы, а не начало escape последовательности. Таким образом, директива ‘#include «x\n\\y»‘ указывает имя файла, содержащего три символа backslash.
'#include ANYTHING ELSE'
Эта модификация называется «вычисляемой директивой #include». Любая директива ‘#include’, не соответствующая ни одной из модификаций, рассмотреных выше, является вычисляемой директивой. Строка ANYTHING ELSE проверяется на наличие соответствующего макроса, значение которого затем заменяет его название. Полученная в результате строка должна уже в точности соответствовать одной из рассмотренных выше модификаций (то есть имя подключаемого файла должно быть заключено в кавычки или угловые скобки).
Эта возможность позволяет определять макросы, что дает возможность изменять имена подключаемых файлов. Эта возможность, например, используется при переносе программ с одной операционной системы на другие, где требуются разные подключаемые файлы.
Директива ‘#include’ указывает С препроцессору обработать указанный файл перед обработкой оставшейся части текущего файла. Информация, выдаваемая препроцессором, содержит уже полученные данные, за которыми следуют данные, получаемые при обработке подключаемого файла, а за которыми, в свою очередь, следуют данные, получаемые при обработке текста, следующего после директивы ‘#include’. Например, дан следующий подключаемый файл ‘header.h’:
char *test ();
и основная программа с именем ‘program.c’, использующая этот файл.
int x; #include «header.h» main ()
Данные, полученные при обработке программы ‘program.c’ будут выглядеть следующим образом:
int x; char *test (); main ()
Для подключаемых файлов нет ограничений на объявления и макроопределения. Любой фрагмент С программы может быть включен в другой файл. Подключаемый файл может даже содержать начало выражения, заканчивающееся в исходном файле или окончание выражения, начало которого находится в исходном файле. Хотя комметарии и строковые константы не могут начинаться подключаемом файле и продолжаться в исходном файле. Не завершенный комментарий, стороковая или символьная константа в подключаемом файле приводят к возникновению ошибки в конце файла.
Подключаемый файл может содержать начало или окончание сиснтаксической конструкции, такой как определение функции.
Срока, следующая за директивой ‘#include’ всегда является пустой и добавляется С препроцессором даже если подключаемый файл не содержит завершающий символ перевода строки.
Часто случается, что подключаемый файл включает в себя другой файл. Это может привести к тому, что отдельный файл будет подключаться неоднократно, что может привести к возникновению ошибок, если файл определяет типы структур или определения типов. Поэтому следует избегать многократного подключения файлов.
Обычно это достигается путем заключения в условие всего содержимого этого файла, как показано ниже:
#ifndef FILE_FOO_SEEN #define FILE_FOO_SEEN Сам файл #endif /* FILE_FOO_SEEN */
Макрос ‘FILE_FOO_SEEN’ указывает на то, что файл уже однажды вкючался. В подключаемых файлах пользователя макрос не должен начинаться с символа ‘_’. В системных подключаемых файлах его имя не должно начинаться с символа ‘__’ во избежание возникновения конфликтов с программами пользователя. Каким бы ни был файл, имя макроса должно содержать имя файла и некоторый дополнительный текст во избежание вознкновения конфликтов с другими подключаемыми файлами.
Препроцессор GNU C построен таким образом, что обработке подключаемого файла он проверяет наличие определенных конструкций и наиболее рационально их обрабатывает. Препроцессор специально отмечает полное вложение файла в условие ‘#ifndef’. Если в подключаемом файле содержится директива ‘#include’, указывающая на обрабатываемый файл, или макрос в директиве ‘#ifndef’ уже определен, то обрабатываемый файл полностью игнорируется.
Существует также специальная директива, указывающая препроцессору, что файл должен быть включен не более одного раза. Эта директива называется ‘#pragma once’. Она использовалась в дополнение к директиве ‘#ifndef’ и в настоящее время она устарела и не должна прменяться.
В объектно ориентированном языке С существует модификация директивы ‘#include’, называемая ‘#import’, которая используется для вкючения файла не более одного раза. При использовании директивы ‘#import’ вместо ‘#include’ не требуется наличия условных оборотов для предотвращения многократной обработки файла.
«Наследование» это то, что происходит, когда какой либо объект или файл образует некоторую часть своего содержимого путем виртуального копирования из другого объекта или файла. В случае подключаемых С файлов наследование означает, что один файл включает другой файл, а затем заменяет или добавляет что-либо.
Если наследуемый подключаемый файл и основной подключаемый файл имеют различные имена, то такое наследование называется прямым. При этом используется конструкция ‘#include «BASE»‘ в наследуемом файле.
Иногда необходимо чтобы у наследуемого и основного подключаемого файла были одинаковые имена.
Например, предположим, что прикладная программа использует системный подключаемый файл ‘sys/signal.h’, но версия файла ‘/usr/include/sys/signal.h’ на данной системе выполняет того, что требуется в прикладной программе. Будет удобнее определить локальную версию, возможно с именем ‘/usr/local/include/sys/signal.h’ для замены или добавления к версии, поставляемой с системой.
Это можно выполнить с применением опции ‘-I.’ при компиляции, а также созданием файла ‘sys/signal.h’ который выполняет требуемые программе функции. Но сделать так, чтобы этот файл включал стандартный файл ‘sys/signal.h’ не так просто. При включении строки ‘#include ‘ в этот файл произойдет подключение новой версии файла, а не стандартной системной версии. Это приведет к рекурсии и ошибке при компиляции.
При использовании директивы `#include ‘ нужный файл будет найден, но этот способ является не эфективным, так как содержит полный путь к системному файлу. Это может отразиться на содержании системы, так как это означает, что любые изменения местоположения системных файлов потребуют дополнительных изменений где-либо еще.
Более эффективным решением этой проблемы является применение директивы ‘#include_next’, которая используется для подключения следующего файла с таким же именем. Эта директива функционирует также как и директива ‘#include’ за исключением поиска требуемого файла. Она начинает поиск списка каталогов подключаемых файлов после каталога, где был найден текущий файл.
Предположим была указана опция ‘-I /usr/local/include’, а список каталогов для поиска включает ‘/usr/include’. Также предположим, что оба каталога содержат файл с именем ‘sys/signal.h’. Директива ‘#include ‘ найдет нужный файл под каталогом ‘/usr/local/include’. Если этот файл содержит строку ‘#include_next ‘, то поиск будет возобновлен после предыдущего каталога и будет найден файл в каталоге ‘/usr/include’.
