Объектный код

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

gcc -c hello.c

Это приведет к созданию файла с расширением *.o. Чтобы преобразовать данный объектный файл в исполняемый, используйте флаг -o:

gcc -o hello hello.o

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

Компиляция програм, состоящих из нескольких исходных файлов

Рассмотрим пример. Допустим, у нас есть две функции в одном файле и функция main() в другом, где они используются.

Файл superprint.c:

#include <stdio.h>
 
void l2r (char **c, int n) {
	int i, j;
	for(i=0; i<n; i++, c++) {
		for (j=0; j<i; j++)
			printf("\t");
		printf ("%s\n",*c);
	}
}
 
void r2l (char **c, int n) {
	int j;
	for(; n>0; n--, c++) {
		for (j=1; j<n; j++)
			printf("\t");
		printf ("%s\n",*c);
	}
}

Файл main.c:

#include <stdio.h>
 
#define N 5
 
main () {
	char strs[N][10];
	char *p[N];
 
	int i;
 
	for(i=0; i<N; i++) {
		scanf("%s", strs[i]);
		p[i] = &strs[i][0];
	}
 
	l2r(p, N);
	r2l(p, N);
}

Функция main() формирует массив из строк и массив указателей на них. Затем передает массив указателей и значение константы N в функции l2r() и r2l(), которые выводят строки в определенном формате.

Для создания исполняемого файла необходимо сначала скомпилировать исходные файлы в объектные файлы:

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() из superprint.c, если нигде в main.c не указано подключение? Если попробовать скомпилировать main.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. Программисту очень трудно следить за вызовами и определениями. Поэтому функции и глобальные константы проекта выносят в отдельный файл, который подключается ко всем исходным файлам. Такие файлы называют заголовочными; мы уже сталкивались с ними. Нестандартные заголовочные файлы для вашего проекта заключаются в кавычки при подключении. Это было отмечено ранее.

Таким образом, правильнее разместить объявление функций в отдельном файле, например myprint.h, и включить прототипы l2r() и r2l(). Затем в main.c нужно вставить директиву:

#include "myprint.h"

В файле superprint.c нет необходимости подключать myprint.h, так как он использует только стандартные функции. Но если в будущем планируется расширение программы, стоить добавлять заголовок заранее.

Теперь подумайте о целесообразности размещения константы N в заголовочном файле? Если ее туда поместить, она станет доступна в обеих файлах. Тогда можно изменить прототипы функций, сохранив только один параметр (указатель), а значение N будет известно из заголовка. Однако при этом второй параметр в r2l() изменяется, что невозможно с константой. Кроме того, возможно, superprint.c станет частью другого проекта с другими константами, где N не будет в заголовке.

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

Особенности использования глобальных переменных

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

В C есть проблема: return возвращает одно значение, но функции могут модифицировать множество переменных, не массива. Тогда без глобальных переменных сложно.

  • Если переменная объявлена за пределами функции (например, int count), она глобальна для всех файлов. Для аccess к ней в aa.c используют имя, если внутри функции такой нет. В других файлах указывают, что это глобальная переменная с помощью extern, например, extern count.
  • В некоторых случаях переменная нужна нескольким функциям в пределах одного файла, но не должна быть видна другим. Тогда ее объявляют с static, например static int count, скрывая ее от других.
Задание:

Напишите простые примеры, чтобы проиллюстрировать использование глобальных функций:

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

Вопросы для самопроверки:

  1. Какой инструмент вызывает компилятор gcc для связывания объектных файлов в единый исполняемый файл?
  2. Какие флаги нужно использовать, чтобы скомпилировать файл в объектный файл и, затем, преобразовать его в исполняемый файл?
  3. Какую роль выполняют заголовочные файлы в программе с несколькими исходными файлами?
  4. Почему не рекомендуется переносить символическую константу в заголовочный файл?
  5. Какие проблемы могут возникнуть при использовании глобальных переменных и как их избежать?

Программа курса:

  1. Описание курса
  2. Введение в язык программирования C
  3. Типы данных в C и форматированный вывод
  4. Символьные типы и управляющие символы в C
  5. Операторы ветвления и switch в C
  6. Циклы и операторы в языке C
  7. Битовые операции в языке C
  8. Посимвольный ввод и вывод в C - буферизация
  9. Переменные, адреса и указатели в C
  10. Передача аргументов по ссылке и значению в C
  11. Форматированный ввод данных с использованием scanf
  12. Генерация псевдослучайных чисел на C
  13. Адресная арифметика в массивах C
  14. Передача массивов в функции и указатели
  15. Строки в языке C - особенности и функции работы
  16. Функции работы со строками в C
  17. Работа со структурами в C - создание и применение
  18. Динамические структуры данных в C
  19. Ввод и вывод данных из файлов в языке C
  20. Передача аргументов в C и работа с файлами
  21. Препроцессор в языке C - директивы и макросы
  22. Создание и компиляция многофайловых программ в C
  23. Использование статических и динамических библиотек в C