Программа на языке Си может компилироваться для различных платформ с учетом различного набора конфигурационных настроек. Мы можем одну и ту же программу компилировать под разные операционные системы. Встроенная библиотека языка Си поддерживает только общий функционал, который может работать на всех системах, но которого может оказаться недостаточно. Но кроме того, каждая операционная система может определять свои специфичные функции, и, возможно, мы захотим использовать эти функции, которые специфичны для каждой платформы.
Кроме того, возможно, мы захотим, чтобы при одних конфигурационных настройках применялись одни функции, при других же настройках - другие функции. Это ставит нас перед проблемой вариативности программы - программа определяет различные варианты, которые будут компилироваться при определенном наборе настроек. Например, если программа компилируется под Linux, вызываем и используем одни функции, если же под Windows - используем другие функции. В общем случае для управления вариантами компиляции применяется препроцессорная директива #ifdef.
Допустим, мы хотим реализовать запись некоторого текста в файл, который будет храниться во вновь созданном каталоге. Но этот каталог в зависимости от флага конфигурации, создается либо в текущей папке, либо в системной папке пользователя (папка, которая называется по имени пользователя). Кроме того, программа должна работать как на Windows, так и на Linux.
Наиболее очевидная и в некотором смысле наивная попытка решить эту задачу представляет определение единого файла программы, который содержит множество директив #ifdef и тем самым позволяет разграничить варианты кода, наподобие следующего:
#include <string.h> #include <stdio.h> #include <stdlib.h> #ifdef __unix__ #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #elif defined _WIN32 #include <windows.h> #endif int main(void) { char dirname[60]; // пусть к папке char filename[60]; // пусть к файлу char* text = "Hello METANIT.COM"; // некоторый текст для сохранения #ifdef __unix__ // если Linux #ifdef HOME_DIR // если сохраняем в домашнем каталоге пользователя sprintf(dirname, "%s%s", getenv("HOME"), "/newdir/"); sprintf(filename, "%s%s", dirname, "newfile"); #elif defined CURRENT_DIR // если сохраняем текущем каталоге strcpy(dirname, "newdir"); strcpy(filename, "newdir/newfile"); #endif mkdir(dirname,S_IRWXU); #elif defined _WIN32 // если Windows #ifdef HOME_DIR // если сохраняем в домашнем каталоге пользователя sprintf(dirname, "%s%s%s", getenv("HOMEDRIVE"), getenv("HOMEPATH"), "\newdir\"); sprintf(filename, "%s%s", dirname, "newfile"); #elif defined CURRENT_DIR // если сохраняем текущем каталоге strcpy(dirname, "newdir"); strcpy(filename, "newdir\newfile"); #endif CreateDirectory (dirname, NULL); #endif // запись файла FILE* f = fopen(filename, "w+"); fwrite(text, 1, strlen(text), f); fclose(f); return 0; }
Здесь мы проверяем установку двух констант. Если установлена константа __unix__
, то мы имеем дело с Linux, если же установлена константа _WIN32
, то программа компилируется для
Windows. В зависимости от установки констант, подключаем те или иные заголовочные файлы:
#ifdef __unix__ #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #elif defined _WIN32 #include <windows.h> #endif
Затем в зависимости от системы в функции main выполняем создание каталога и запись в него файла:
#ifdef __unix__ // если Linux #ifdef HOME_DIR // если сохраняем в домашнем каталоге пользователя sprintf(dirname, "%s%s", getenv("HOME"), "/newdir/"); sprintf(filename, "%s%s", dirname, "newfile"); #elif defined CURRENT_DIR // если сохраняем текущем каталоге strcpy(dirname, "newdir"); strcpy(filename, "newdir/newfile"); #endif mkdir(dirname,S_IRWXU); #elif defined _WIN32 // если Windows #ifdef HOME_DIR // если сохраняем в домашнем каталоге пользователя sprintf(dirname, "%s%s%s", getenv("HOMEDRIVE"), getenv("HOMEPATH"), "\newdir\"); sprintf(filename, "%s%s", dirname, "newfile"); #elif defined CURRENT_DIR // если сохраняем текущем каталоге strcpy(dirname, "newdir"); strcpy(filename, "newdir\newfile"); #endif CreateDirectory (dirname, NULL); #endif
Если установлена константа HOME_DIR каталог создается в домашней папке пользователя, а если установлена константа CURRENT_DIR
- в текущей папке программы.
То есть фактически здесь определены две разные реализации для конкретной операционной системы, помещенные в один файл. Однако в таком коде трудно ориентироваться.
Платформо-зависимый и платформо-независимый код смешены в одной функции. А что, если добавить еще одно
выражение #ifdef
для еще одной операционной системы? Поддерживаемость и читабельность кода будет стремиться к нулю. Посмотрим, как исправить эту ситуацию.
Самое простое решение состоит в вынесении кода, который работает с вариантами, в отдельные функции. Затем эти функции вызываются из основной части программы, которая будет содержать только независимый от платформы код:
#include <string.h> #include <stdio.h> #include <stdlib.h> #ifdef __unix__ #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #elif defined _WIN32 #include <windows.h> #endif void getDirectoryName(char* dirname) { #ifdef __unix__ #ifdef HOME_DIR sprintf(dirname, "%s%s", getenv("HOME"), "/newdir/"); #elif defined CURRENT_DIR strcpy(dirname, "newdir/"); #endif #elif defined _WIN32 #ifdef HOME_DIR sprintf(dirname, "%s%s%s", getenv("HOMEDRIVE"), getenv("HOMEPATH"), "\newdir\"); #elif defined CURRENT_DIR strcpy(dirname, "newdir\"); #endif #endif } void createNewDirectory(char* dirname) { #ifdef __unix__ mkdir(dirname,S_IRWXU); #elif defined _WIN32 CreateDirectory (dirname, NULL); #endif } int main(void) { char dirname[60]; char filename[60]; char* text = "Hello METANIT.COM"; getDirectoryName(dirname); createNewDirectory(dirname); sprintf(filename, "%s%s", dirname, "newfile"); FILE* f = fopen(filename, "w+"); fwrite(text, 1, strlen(text), f); fclose(f); return 0; }
Теперь платформо-зависимый код выделен в две функции - getDirectoryName (для получения пути к папке) и createNewDirectory (для создания новой папки). В таком коде гораздо проще ориентироваться.
Тем не менее здесь сохраняется другая проблема - дублирование кода - мы дублируем проверку констант __unix__
и _WIN32
, чтобы определить текущую систему.
И также дублируем проверку констант HOME_DIR и CURRENT_DIR
, чтобы определить каталог. А если мы захотим добавить поддержку еще одной платформы, то нам придется менять код в нескольких местах.
Частичное решение проблемы дублирования кода проверки условий компиляции в общем случае заключается в вынесении работы с отдельными вариантами в отдельные примитивы - функции:
#include <string.h> #include <stdio.h> #include <stdlib.h> #ifdef __unix__ #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #elif defined _WIN32 #include <windows.h> #endif void getHomeDirectory(char* dirname) { #ifdef __unix__ sprintf(dirname, "%s%s", getenv("HOME"), "/newdir/"); #elif defined _WIN32 sprintf(dirname, "%s%s%s", getenv("HOMEDRIVE"), getenv("HOMEPATH"), "\newdir\"); #endif } void getCurrentDirectory(char* dirname) { #ifdef __unix__ strcpy(dirname, "newdir/"); #elif defined _WIN32 strcpy(dirname, "newdir\"); #endif } void getDirectoryName(char* dirname) { #ifdef HOME_DIR getHomeDirectory(dirname); #elif defined CURRENT_DIR getCurrentDirectory(dirname); #endif } void createNewDirectory(char* dirname) { #ifdef __unix__ mkdir(dirname,S_IRWXU); #elif defined _WIN32 CreateDirectory (dirname, NULL); #endif } int main(void) { char dirname[60]; char filename[60]; char* text = "Hello METANIT.COM"; getDirectoryName(dirname); createNewDirectory(dirname); sprintf(filename, "%s%s", dirname, "newfile"); FILE* f = fopen(filename, "w+"); fwrite(text, 1, strlen(text), f); fclose(f); return 0; }
Теперь получание пути к папке разбита на две функции - два варианта - для домашней папки пользователя и для текущей папки:
void getHomeDirectory(char* dirname) { #ifdef __unix__ sprintf(dirname, "%s%s", getenv("HOME"), "/newdir/"); #elif defined _WIN32 sprintf(dirname, "%s%s%s", getenv("HOMEDRIVE"), getenv("HOMEPATH"), "\newdir\"); #endif } void getCurrentDirectory(char* dirname) { #ifdef __unix__ strcpy(dirname, "newdir/"); #elif defined _WIN32 strcpy(dirname, "newdir\"); #endif }
Затем в функции getDirectoryName в зависимости от установки констант вызываем ту или иную функцию:
void getDirectoryName(char* dirname) { #ifdef HOME_DIR getHomeDirectory(dirname); #elif defined CURRENT_DIR getCurrentDirectory(dirname); #endif }
Это позволило избавиться от проверки одной категории вариантов - HOME_DIR/CURRENT_DIR, однако дублирование проверки на операционную систему сохраняется. Кроме того, вынесение вариантов в отдельные функции несколько увеличивает сложность программы, код становится длинее, что опять же может осложнить его поддержку. В следующей статье посмотрим, как мы можем решить эту проблему.