Для упрощения организации программы ее код обычно разбивается на отдельные модули или отдельные файлы. Отдельный файл/модуль обычно содержит связанный функционал, который выполняет некоторую общюю работу. Однако при разделении функционала программы на отдельные файлы мы можем столкнуться с рядом проблем.
Частая встречающаяся проблема представляет дублирование глобальных идентификторов в разных файлах. Например, пусть у нас есть файл (модуль) user.c:
#include <stdio.h> // условные данные char* username = "tom@smail.com"; void printUserName() { printf("UserName: %s\n", username); } void resetUserName() { username = "Undefined"; }
Здесь определена глобальная переменная username и две функции для ее вывода на консоль и сброса в некоторое стандартное значение. И допустим, у нас есть основной файл программы app.c, который использует модуль "user.c":
#include <stdio.h> char* username = "Tom"; extern void resetUserName(); void printUserName() { printf("Name: %s\n", username); } int main(void) { printUserName(); resetUserName(); }
Здесь определены глобальная переменная и функция с теми же именами, что и в модуле "user.c", но немного иным содержимым. Попробуем скомпилировать оба файла, например, с помощью gcc
gcc -Wall -pedantic app.c user.c -o app
И в процессе компиляции мы столкнемся с ошибкой. В зависимости от среды и компилятора конкретный вывод ошибки может отличаться, но в любом случае ее суть будет в том, что у нас дублируются глобальные идентификаторы в обоих файлах - глобальная переменная username и функция printUserName.
Вторая проблема касается несанкционированного доступа к функционалу. Так, в коде "app.c" мы вызываем функцию resetUserName для сброса имени. Однако здесь мы можем задаться вопросом, а должен ли внешний код иметь возможность выполнять эту функцию? В зависимости от задачи и ситуации ответ может быть разным. Но, допустим, мы НЕ хотим, чтобы внешний код видел эти данные. Особенно если модуль "user.c" создаем мы, а использует его какое-то внешнее приложение, где мы вообще можем не знать, в каком случае эта функция будет вызываться.
Обе проблемы ставят нас перед задачей инкапсуляции функционала в модуле и предоставления внешнему коду доступа только к определенным ограниченным возможностям модуля.
В общем случае при определении модуля прежде всего определяется API (Application Program Interface) - тот функционал, который будет виден извне и который может использоваться во внешнем коде. Объявления функций и типов, которые входят в API, выносятся в заголовочный файл, для его подключения во внешнем коде. Остальной функционал, который не попадает в API, определяется с модификатором static, что позволяет сделать этот функционал доступным только в рамках файла определения.
Например, нам надо определить модуль для вывода текстового файла на консоль. Вначале определим API и для этого создадим следующий файл file_printer.h:
// функция вывода файла на консоль void printFile(char*);
API довольно простое - определяем функцию printFile, которая принимает имя файла.
Далее определим модуль поиска в файле - файл file_printer.c, где будет располагаться реализация вывода файла на консоль:
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <assert.h> #include "file_printer.h" // получаем длину файла // функция не входит в API, поэтому определена со словом static static int getFileLength(FILE* fp) { fseek(fp, 0, SEEK_END); int file_length = ftell(fp); return file_length; } // считываем файл в buffer // функция не входит в API, поэтому определена со словом static static void readFile(FILE* fp, char* buffer, int file_length) { fseek(fp, 0, SEEK_SET); int read_elements = fread(buffer, 1, file_length, fp); buffer[read_elements] = '\0'; // устанавливем концевой нулевой байт для строки } // вывод файла на консоль // Функция printFile входит в API, поэтому определена без слова static void printFile(char* filename) { FILE* fp = 0; // инициализируем нулем char* text = 0; // инициализируем нулем assert(filename && "Invalid file name"); // если некорректное имя файла, прерываем программу fp = fopen(filename, "r"); // открываем файл assert(fp && "Unable to open file!\n"); // если не удалось открыть файл, прерываем программу int file_length = getFileLength(fp); // получаем длину файла if(file_length > 0) { text = malloc(file_length); if(text) // если удалось выделять память { readFile(fp, text, file_length); // считываем файл printf("%s\n",text); free(text); // освобождаем память text = 0; // сбрасываем значение указателя в ноль } } fclose(fp); // закрываем файл fp = 0; // сбрасываем значение указателя в ноль }
Здесь определена реализация функции printFile для вывода файла на консоль. Однако в своей сущности она использует две другие функции. Так, чтобы вывести файл на консоль, нам надо сначала получить его длину с помощью функции getFileLength и затем считать в буфер полученной длины файл с помощью функции readFile. Использование этих функций в отрыве от функции printFile не имеет смысла. Внешний код не должен к ним обращаться, поэтому они не входят в API и определены с помощью ключевого слова static
И определим основной файл программы app.c, который будет использовать вывод файла на консоль:
#include <stdio.h> #include "file_printer.h" int main(void) { char* filename ="test.txt"; printFile(filename); }
Для использования API подключаем его определение в файле "file_printer.h". Таким образом, для внешнего кода - файла "app.c" существует только функция printFile, а ее реализация для него не важны.
Таким образом, мы можем обеспечить инкапсуляцию функционала и отделить определение API от его реализации.