Необычное использование файла .h в C

Во время чтения статьи о фильтрации я нашел странное использование файла .h - используйте его для заполнения массива коэффициентов:

#define N 100 // filter order
float h[N] = { #include "f1.h" }; //insert coefficients of filter
float x[N];
float y[N];

short my_FIR(short sample_data)
{
  float result = 0;

  for ( int i = N - 2 ; i >= 0 ; i-- )
  {
    x[i + 1] = x[i];
    y[i + 1] = y[i];
  }

  x[0] = (float)sample_data;

  for (int k = 0; k < N; k++)
  {
    result = result + x[k]*h[k];
  }
  y[0] = result;

  return ((short)result);
}

Итак, нормальная ли практика использования float h[N] = { #include "f1.h" }; таким образом?

Ответ 1

Preprocessor директивы типа #include просто выполняют некоторую текстовую подстановку (см. документацию GNU cpp внутри GCC). Это может произойти в любом месте (вне комментариев и строковых литералов).

Однако a #include должен иметь свой # как первый непустой символ своей строки. Таким образом, вы будете кодировать

float h[N] = {
  #include "f1.h"
};

Исходный вопрос не содержал #include в своей строке, поэтому имел неправильный код.

Это не обычная практика, но это разрешено практикой. В этом случае я бы предложил использовать другое расширение, кроме .h, например. используйте #include "f1.def" или #include "f1.data"...

Попросите своего компилятора показать вам предварительно обработанную форму. С GCC скомпилируйте с помощью gcc -C -E -Wall yoursource.c > yoursource.i и посмотрите с помощью редактора или пейджера в сгенерированный yoursource.i

Я действительно предпочитаю иметь такие данные в своем исходном файле. Поэтому я хотел бы предложить создать автономный файл h-data.c, используя, например, какой-то инструмент вроде GNU awk (так что файл h-data.c начинался с const float h[345] = { и заканчивался на };...) И если это постоянные данные, лучше объявите его const float h[] (чтобы он мог сидеть в сегменте только для чтения, например .rodata в Linux). Кроме того, если встроенные данные большие, компилятор может потратить некоторое время на (бесполезно) оптимизировать его (тогда вы можете быстро скомпилировать h-data.c без оптимизации).

Ответ 2

Итак, нормально ли использовать float h [N] = {#include "f1.h" }; таким образом?

Это не нормально, но он действителен (будет принят компилятором).

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

Недостатки:

  • он увеличивает коэффициент WTF/SLOC вашего кода.
  • он вводит необычный синтаксис как в коде клиента, так и во включенном коде.
  • чтобы понять, что делает f1.h, вам нужно будет посмотреть, как он используется (это означает, что вам нужно добавить дополнительные документы в свой проект, чтобы объяснить этого зверя, или людям придется прочитать код для см., что это означает - ни одно решение не приемлемо).

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

Ответ 3

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

Вот альтернативное решение:

Файл f1.h:

#ifndef F1_H
#define F1_H

#define F1_ARRAY                   \
{                                  \
     0, 1, 2, 3, 4, 5, 6, 7, 8, 9, \
    10,11,12,13,14,15,16,17,18,19, \
    20,21,22,23,24,25,26,27,28,29, \
    30,31,32,33,34,35,36,37,38,39, \
    40,41,42,43,44,45,46,47,48,49, \
    50,51,52,53,54,55,56,57,58,59, \
    60,61,62,63,64,65,66,67,68,69, \
    70,71,72,73,74,75,76,77,78,79, \
    80,81,82,83,84,85,86,87,88,89, \
    90,91,92,93,94,95,96,97,98,99  \
}

// Values above used as an example

#endif

Файл f1.c:

#include "f1.h"

float h[] = F1_ARRAY;

#define N (sizeof(h)/sizeof(*h))

...

Ответ 4

Нет, это не нормальная практика.

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


Однако существует "шаблон", который включает в себя файл в таких случайных местах: X-Macros, такой как те.

Использование X-macro заключается в том, чтобы определить коллекцию один раз и использовать ее в разных местах. Единое определение, обеспечивающее согласованность целого. В качестве тривиального примера рассмотрим:

// def.inc
MYPROJECT_DEF_MACRO(Error,   Red,    0xff0000)
MYPROJECT_DEF_MACRO(Warning, Orange, 0xffa500)
MYPROJECT_DEF_MACRO(Correct, Green,  0x7fff00)

который теперь можно использовать несколькими способами:

// MessageCategory.hpp
#ifndef MYPROJECT_MESSAGE_CATEGORY_HPP_INCLUDED
#define MYPROJECT_MESSAGE_CATEGORY_HPP_INCLUDED

namespace myproject {

    enum class MessageCategory {
#   define MYPROJECT_DEF_MACRO(Name_, dummy0_, dummy1_) Name_,
#   include "def.inc"
#   undef MYPROJECT_DEF_MACRO
    NumberOfMessageCategories
    }; // enum class MessageCategory

    enum class MessageColor {
#   define MYPROJECT_DEF_MACRO(dumm0_, Color_, dummy1_) Color_,
#   include "def.inc"
#   undef MYPROJECT_DEF_MACRO
    NumberOfMessageColors
    }; // enum class MessageColor

    MessageColor getAssociatedColorName(MessageCategory category);

    RGBColor getAssociatedColorCode(MessageCategory category);

} // namespace myproject

#endif // MYPROJECT_MESSAGE_CATEGORY_HPP_INCLUDED

Ответ 5

Долгое время люди злоупотребляли препроцессором. См. Например, формат файла XPM, который был разработан так, чтобы люди могли:

#include "myimage.xpm"

в их коде C.

Это больше не считается хорошим.

Код OP выглядит как C, поэтому я расскажу о C

Почему это чрезмерное использование препроцессора?

Директива препроцессора #include предназначена для включения исходного кода. В этом случае и в случае OP это не настоящий исходный код, а данные.

Почему это считается плохим?

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

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

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

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

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

Когда это приемлемо?

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

Следует отметить, что отсутствие доступа к файлам означает, что вы программируете для очень ограниченной платформы, и поэтому вы должны делать компромиссы. Это будет иметь место, если ваш код работает на микроконтроллере, например.

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

Например, предоставление реальной функции C, возвращающей коэффициенты, вместо того, чтобы иметь полуформатный файл данных. Эта функция C затем может быть определена в двух разных файлах: одна с использованием ввода-вывода для целей разработки и enother одна возвращающая статические данные для сборки релиза. Вы должны скомпилировать корректный исходный файл conditionnaly.

Ответ 6

Иногда возникают ситуации, которые требуют либо использования внешних инструментов для генерации .C файлов на основе других файлов, содержащих исходный код, имеющих внешние инструменты, генерирующие файлы C с чрезмерным количеством кода, жестко подключаемым к генерирующим инструментам, или имеющие кода используйте директиву #include в различных "необычных" способах. Из этих подходов я бы предположил, что последнее - хотя и нехорошее - часто может быть наименее злым.

Я бы предложил избегать использования суффикса .h для файлов, которые не соблюдаются нормальными соглашениями, связанными с файлами заголовков (например, путем включения определений методов, выделения пространства, требующих необычного контекста включения (например, в середине метод), требующий множественного включения с различными макросами и т.д. Я также вообще избегаю использования .c или .cpp для файлов, которые включены в другие файлы через #include, если эти файлы в основном не используются автономно [я мог бы в некоторых например, есть файл fooDebug.c, содержащий #define SPECIAL_FOO_DEBUG_VERSION [newline] `#include "foo.c" ` `, если я хочу иметь два объектных файла с разными именами, сгенерированными из того же источника, и один из них утвердительно - нормальной "версии.]

Моя нормальная практика заключается в использовании .i в качестве суффикса для сгенерированных человеком или машинных файлов, которые предназначены для включения, но обычными способами, из других исходных файлов C или С++; если файлы сгенерированы машиной, как правило, инструмент генерации включает в качестве первой строки комментарий, идентифицирующий инструмент, используемый для его создания.

BTW, один трюк, где я использовал это, когда я хотел разрешить создание программы с использованием только командного файла без каких-либо сторонних инструментов, но хотел подсчитать, сколько раз он был создан. В моем пакетном файле я включил echo +1 >> vercount.i; то в файле vercount.c, если я правильно помню:

const int build_count = 0
#include "vercount.i"
;

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

Ответ 7

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

Например f1.h может выглядеть так:

#ifndef _f1_h_
#define _f1_h_

#ifdef N
float h[N] = {
    // content ...
}

#endif // N

#endif // _f1_h_

И файл .c:

#define N 100 // filter order
#include "f1.h"

float x[N];
float y[N];
// ...

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

Ответ 8

Добавление к тому, что говорили все остальные, - содержимое f1.h должно быть таким:

20.0f, 40.2f,
100f, 12.40f
-122,
0

Потому что текст в f1.h будет инициализировать массив, о котором идет речь!

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

Ответ 9

Это обычная практика для меня.

Препроцессор позволяет разделить исходный файл на столько фрагментов, сколько захотите, которые собраны директивами #include.

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

Я также использую их, когда некоторые части кода автоматически генерируются каким-то внешним инструментом: очень удобно, чтобы инструмент просто генерировал свои фрагменты и включал их в остальную часть кода, написанного вручную.

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

По традиции директива #include была использована для включения файлов заголовков, то есть наборов деклараций, которые выставляют API. Но ничего не обязывает.

Ответ 10

Когда препроцессор находит директиву #include, он просто открывает указанный файл и вставляет его содержимое, как если бы содержимое файла было записано в месте расположения директивы.

Ответ 11

Я читал, что люди хотят рефакторировать и говорят, что это зло. Тем не менее я использовал в некоторых случаях. Как некоторые люди сказали, что это предпроцессорная директива, поэтому включает в себя содержимое файла. Здесь случай, когда я использовал: построение случайных чисел. Я строю случайные числа, и я не хочу делать это каждый раз, когда я не компилирую ни во время выполнения. Поэтому другая программа (обычно script) просто заполняет файл сгенерированными номерами, которые включены. Это позволяет избежать копирования вручную, что позволяет легко изменять числа, алгоритм, который генерирует их и другие тонкости. Вы не можете легко обвинять практику, в этом случае это просто правильный способ.

Ответ 12

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

Я изолировал сгенерированные файлы .h в отдельной папке, чтобы их можно было легко идентифицировать:

#include "gensrc/myfile.h"

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

Я представил образцы в список рассылки Eclipse, но, похоже, не было большого интереса к "исправлению" проверки синтаксиса.

Я изменил свой генератор кода, чтобы принять дополнительные аргументы, чтобы он мог сгенерировать это объявление всей переменной, а не только данные. Теперь он генерирует синтаксически корректные файлы include.

Даже если я не использовал Eclipse, я думаю, что это лучшее решение.

Ответ 13

В ядре Linux я нашел пример, который является IMO, красивым. Если вы посмотрите на заголовочный файл cgroup.h

http://lxr.free-electrons.com/source/include/linux/cgroup.h

вы можете найти директиву #include <linux/cgroup_subsys.h>, используемую дважды, после различных определений макроса SUBSYS(_x); этот макрос используется внутри cgroup_subsys.h, чтобы объявить несколько имен групп Linux (если вы не знакомы с группами, это удобные для пользователей интерфейсы Linux, которые должны быть инициализированы при загрузке системы).

В фрагменте кода

#define SUBSYS(_x) _x ## _cgrp_id,
enum cgroup_subsys_id {
#include &ltlinux/cgroup_subsys.h&gt
   CGROUP_SUBSYS_COUNT,
};
#undef SUBSYS

каждый SUBSYS(_x), объявленный в cgroup_subsys.h, становится элементом типа enum cgroup_subsys_id, а в фрагменте кода

#define SUBSYS(_x) extern struct cgroup_subsys _x ## _cgrp_subsys;
#include &ltlinux/cgroup_subsys.h&gt
#undef SUBSYS

каждый SUBSYS(_x) становится объявлением переменной типа struct cgroup_subsys.

Таким образом, программисты ядра могут добавлять группы, изменяя только cgroup_subsys.h, в то время как предварительный процессор автоматически добавляет связанные значения перечисления/декларации в файлы инициализации.