Незвичне використання файлу .h у C


77

Під час читання статті про фільтрацію я виявив дивне використання .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" };цей спосіб?


25
Це незвично. Це означає, що він використовується рідко: f1.hможе генеруватися зовнішнім інструментом і використовуватись як вхід для вашої (скомпільованої) програми. Частіше зустрічається, що зовнішні інструменти генерують повномасштабний файл заголовка, наприклад float h[N] = { ... }, всередині f1.h. Приклад тут .
ta.speot.is

8
Він не повинен компілюватися, див. Синтаксис у C11 6.10.2. # include "q-char-sequence" new-line. Де нова лінія?
Лундін,

2
Тож ... як усім цим людям, які кажуть, що це дійсний код, вдається змусити його скомпілювати? Я не можу керувати використанням gcc, я отримую error: "stray #"незалежно від того, який стандарт чи параметри я передаю.
Лундін,

2
@Lundin: Деякі компілятори дозволяють біти препроцесора де завгодно, а в решту ви можете просто вставити нові рядки. Не порушує цей конкретний приклад.
Mooing Duck

2
@ User657267: Це є звичайною практикою рідко випадків. У моїй кар’єрі було близько трьох-чотирьох випадків, коли це врятувало мені день. Як писав Джейсон Р, коефіцієнти, на щастя, включені таким чином (без синтаксичної помилки у прикладі). Як правило, імпортовані дані, які ви хочете мати у своїй програмі. Що стосується коду, це читабельність, а наявність екзотичних синтаксичних аналізаторів чи граматик просто для створення акуратного файлу C або C ++ при кожному зміні даних буде набагато менш читабельним.
Себастьян Мах

Відповіді:


132

Препроцесорні директиви, як-от #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швидко скомпілювати без оптимізації).


3
навіщо перенаправляти висновок, а не просто використовувати gcc -C -E -o yourource.i -Wall yourource.c? Там варіант є з причини!
Дейв

26
Тому що >один персонаж коротший за -oі тому, що я кульгаю.
Василь Старинкевич

10
Власне, мій перший Unix був на Sun3, SunOS3.2 ... (так 1987 р.)
Базиль Старинкевич

10

Отже, чи є звичайною практика використовувати float h [N] = {#include “f1.h”}; сюди?

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

Переваги використання цього: це позбавляє вас невеликої кількості зусиль, необхідних для кращого рішення.

Недоліки:

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

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


4
Відсутні нові рядки, вони #includeповинні бути в окремому рядку.
Василь Старинкевич

Як зазначає Старинкевич, цей код С буде скомпільовано з директивою include, оточеною новими рядками. Див. ISO / IEC 9899 (C11) §6.10 2 "Директива попередньої обробки складається з послідовності маркерів попередньої обробки, яка починається з # маркера попередньої обробки" ... "або яка йде після пробілу, що містить принаймні один символ нового рядка, і закінчується наступним символом нового рядка. " "...". Що надає f1.h, видно з контексту. Це буде список виразів, відокремлених комами, сумісних з float, розміром N. Я б припустив, що ці коефіцієнти в будь-якому випадку виходять з окремого стандарту або документа.
user1155120

1
Що таке "нормальне"? Я вважаю, що це хороша практика, коли це дає кращу читабельність імпортованих даних, ніж наявність екзотичних та складних тригерів, які, в свою чергу, створюють акуратні вихідні файли. Синтаксис незвичний, але для досвідченого програміста на С або С ++, який знає, що робить препроцесор, це має бути несподіванкою, але не великим WTF.
Себастьян Мах

@BasileStarynkevitch, я знаю, що це має бути в окремому рядку. Мені було більше цікаво відповісти на запитання, поставлене ОП, ніж виправити код.
утнапістім

@phresnel, у своєму професійному досвіді (більше 8 років C ++) я бачив це лише один раз у виробничому коді, і саме тоді Стівен Лававей пояснював, як вони генерували спеціалізації std :: function із змінною кількістю параметрів, включивши параметри в шаблонних деклараціях (я думаю, це було до С ++ 11). Ось чому я вважаю це нетиповим ("не нормальним"). Акуратні вихідні файли та хороша читабельність імпортованих даних не є взаємовиключними (у цьому відношенні це помилковий вибір).
утнапістім

10

Як вже пояснювалось у попередніх відповідях, це не є звичайною практикою, але є дійсною.

Ось альтернативне рішення:

Файл 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))

...

7
І такий підхід «спалює» символи F1_ARRAYі F1_H. Коли у вас є сформований файл, може бути корисно уникати використання символів препроцесора, подібних цим. Для створеного людиною заголовного файлу краще рішення, але не для набору сформованих файлів даних.
харпер

4
Я не розумію, чому це краще, ніж існуюче рішення. Зараз важче автоматично генерувати цей файл даних, і це забруднює простір символів. Як вже зазначає Харпер, для генерованих людиною даних це може бути і краще, але все ж гірше, ніж дозволяти людям генерувати їх усередині справжнього вихідного файлу. Якщо це занадто складно, людське покоління, мабуть, все-таки помиляється.
Себастьян Мах

@phresnel: Чому важче автоматично згенерувати цей файл даних? Він просто включає ще 6 рядків постійного тексту (4 директиви препроцесора та 2 фігурні дужки).
barak manos

@harper: Дякуємо за ваш коментар. Для набору сформованих файлів даних, оскільки кожен файл має унікальну назву, ви можете легко створити пару унікальних символів препроцесора для кожного файлу. Створення цих постійних текстових рядків за допомогою сценарію також не повинно бути надто складним.
barak manos

@barakmanos: Саме так; тепер вам потрібно якось надрукувати ці зайві рядки. Ви повинні відповідати вашій існуючій програмі Python або Haskell не тільки для виведення значень, розділених комами, але і для C-коду. Якщо ваші коефіцієнти надходять від власного постачальника, наприклад, від лабораторії або від якогось обчислення відбиття світла, тепер вам доведеться викликати додаткові catта ін., І ви повинні переконатися, що ваш catта скрипти дійсно запущені.
Себастьян Мах

9

Ні, це не нормальна практика.

Від безпосереднього використання такого формату майже немає переваг , натомість дані можуть бути сформовані в окремому вихідному файлі або, принаймні, у цьому випадку може бути сформовано повне визначення.


Існує, однак, «шаблон» , який включає в себе в тому числі файлу в таких випадкових місцях: X-макроси , такі як ті .

Використання X-макросу полягає в тому, щоб визначити колекцію один раз і використовувати її в різних місцях. Єдине визначення, що забезпечує узгодженість цілого. Як тривіальний приклад розглянемо:

// 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

7

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

#include "myimage.xpm"

в їх коді C.

Це вже не вважається хорошим.

Код ОП виглядає Cтак, про що я поговорюC

Чому це зловживання препроцесором?

#includeДиректива препроцесора призначена для включення вихідного коду. У цьому випадку та у випадку з OP це не справжній вихідний код, а дані .

Чому це вважається поганим?

Бо це дуже негнучко . Ви не можете змінити зображення без перекомпіляції всієї програми. Ви навіть не можете включити два зображення з однаковою назвою, тому що це створить некомпілюючий код. У випадку з ОП він не може змінити дані без перекомпіляції програми.

Інша проблема полягає в тому, що це створює тісний зв'язок між даними та вихідним кодом , наприклад файл даних повинен містити принаймні кількість значень, визначених Nмакросом, визначеним у файлі вихідного коду.

Тісне зв’язування також накладає формат на ваші дані, наприклад, якщо ви хочете зберегти значення матриці 10x10, ви можете вибрати використовувати одновимірний масив або двовимірний масив у своєму вихідному коді. Перехід від одного формату до іншого призведе до змін у вашому файлі даних.

Ця проблема завантаження даних буде легко вирішена за допомогою стандартних функцій вводу / виводу. Якщо вам дійсно потрібно включити деякі зображення за замовчуванням, ви можете вказати шлях за замовчуванням до зображень у своєму вихідному коді. Це щонайменше дозволить користувачеві змінити це значення (за допомогою параметра #defineабо -Dпід час компіляції) або оновити файл зображення без необхідності перекомпілювати.

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

Коли це прийнятно?

Коли ви не можете зробити динамічне завантаження даних. У цьому випадку вам доведеться завантажувати свої дані статично і ви змушені використовувати такі методи.

Слід зазначити, що відсутність доступу до файлів означає, що ви програмуєте для дуже обмеженої платформи , і тому вам доведеться робити компроміси. Це буде так, якщо ваш код працює, наприклад, на мікроконтролері.

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

Наприклад, надання реальної Cфункції, що повертає коефіцієнти, а не наявність напівформатованого файлу даних. Потім цю Cфункцію можна було визначити у двох різних файлах, один з яких використовує введення-виведення для цілей розробки, а інший повертає статичні дані для побудови випуску. Ви скомпілюєте правильний вихідний файл умовно.


@YvesDaoust З одного боку, це величезна діра в безпеці. Оскільки XPM - це файли зображень, ви часто отримуєте їх з інших джерел, а не робите їх самостійно. Зловмисний користувач міг легко змінити файл XPM, щоб закінчити рядок, і таким чином легко ввести код.
trlkly

@trlkly: А як зловмисний користувач отримує доступ до вихідних кодів та систем контролю версій активів, а також до доступу до виробничої машини збірки? З одного боку, ви погано знаєте C або C ++.
Себастьян Мах

"Директива препроцесора #include призначена для включення вихідного коду": препроцесору все одно, він створює повний файл із шматків, а компілятор не знає. (Це може мати значення для попередньо скомпільованих заголовків, але вони не належать до стандарту.) Я використовую їх, коли це чистіше робити.
Ів Дауст,

@YvesDaoust Ви, звичайно, можете використовувати препроцесор C, щоб робити будь-яку заміну макросу, яку ви хочете, для будь-якого типу файлу, який ви хочете. Але моя відповідь була в контексті цього питання. Препроцесор C названий препроцесором C з причини: він призначений для попередньої обробки файлів C. І, нарешті, файли C мають стати вихідним кодом. Отже, я вважаю вірним сказати, що: директива, призначена для включення файлів, призначених для використання препроцесором, сама призначена для попередньої обробки вихідного коду, це директива, призначена для включення вихідного коду. :)
fjardon

@phresnel Про що ти говориш? Вони мають прямий доступ до вихідного коду, оскільки неправдивий файл XPM щойно був #includeфайлом da. Саме такий сценарій обговорюється у питанні. XPM має доступ до середовища збірки, оскільки саме сюди входить файл. Звичайно, якщо ви взагалі розумні, ви перевірите XPM, щоб переконатися, що він дійсний, перш ніж #includeвводити його, але це все ще є серйозним недоліком безпеки.
trlkly

5

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

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

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

До речі, одним із прийомів, коли я це використав, було те, коли я хотів дозволити програму будувати, використовуючи лише командний файл, без сторонніх інструментів, але хотів порахувати, скільки разів вона була побудована. У своєму пакетному файлі я включив echo +1 >> vercount.i; то у файлі vercount.c, якщо я правильно згадую:

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

Кінцевий ефект полягає в тому, що я отримую значення, яке збільшується при кожній збірці без необхідності покладатися на будь-які сторонні інструменти для її створення.


3

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


1
Не забувайте, що включений файл буде попередньо оброблений, а потім також скомпільований.
Себастьян Мах

3

Як уже було сказано в коментарях, це не є звичайною практикою. Якщо я бачу такий код, я намагаюся його рефакторингувати.

Наприклад, це 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];
// ...

Мені це здається трохи більш нормальним - хоча наведений вище код можна ще вдосконалити (наприклад, усунувши глобалісти).


3

Додавання до того, що сказали всі інші, - зміст f1.h повинен бути таким:

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

Тому що текст в f1.h буде ініціалізувати відповідний масив!

Так, він може містити коментарі, інші функції або використання макросів, вирази тощо.


3

Для мене це звичайна практика.

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

Це має великий сенс, коли ви не хочете захаращувати код довгими / не для читання розділами, такими як ініціалізація даних. Як виявляється, мій файл "ініціалізація масиву" має довжину 11000 рядків.

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

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

За традицією директива #include використовувалась для включення файлів заголовків, тобто наборів оголошень, що виставляють API. Але ніщо цього не вимагає.


2

Я читав, що люди хочуть рефакторинг і кажуть, що це зло. І все-таки я використовував у деяких випадках. Як сказали деякі люди, це директива препроцесора, тому включає вміст файлу. Ось випадок, коли я використовував: побудову випадкових чисел. Я будую випадкові числа, і я не хочу робити це кожного разу, коли не компілюю жодного під час виконання. Отже, інша програма (зазвичай це скрипт) просто заповнює файл згенерованими номерами, які включені. Це дозволяє уникнути копіювання вручну, це дозволяє легко змінювати числа, алгоритм їх генерування та інші приємності. Ви не можете легко звинуватити практику, в такому випадку це просто правильний шлях.


1
Чому б вам не побудувати випадкові числа під час компіляції? Цей варіант використання мені здається не надто дійсним, як є.
Себастьян Мах

2

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

Я виділив створені файли .h в окрему папку, щоб їх можна було легко ідентифікувати:

#include "gensrc/myfile.h"

Ця схема розпалася, коли я почав використовувати Eclipse. Перевірка синтаксису Eclipse була недостатньо складною, щоб впоратися з цим. Він реагував би повідомленням про синтаксичні помилки там, де їх не було.

Я повідомив зразки до списку розсилки Eclipse, але, схоже, не було великого інтересу до "виправлення" перевірки синтаксису.

Я змінив свій генератор коду, щоб взяти додаткові аргументи, щоб він міг генерувати всю декларацію змінної, а не лише дані. Тепер він генерує синтаксично правильні файли включення.

Навіть якщо я не використовував Eclipse, я вважаю, що це краще рішення.


1

У ядрі 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-груп (якщо ви не знайомі з cgroups, це зручний інтерфейс, який пропонує Linux, який потрібно ініціалізувати під час завантаження системи).

У фрагменті коду

#define SUBSYS(_x) _x ## _cgrp_id,
enum cgroup_subsys_id {
#include <linux/cgroup_subsys.h>
   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 <linux/cgroup_subsys.h>
#undef SUBSYS

кожен SUBSYS(_x)стає оголошенням змінної типу struct cgroup_subsys.

Таким чином, програмісти ядра можуть додавати cgroups, модифікуючи лише cgroup_subsys.h, тоді як попередній процесор автоматично додаватиме відповідні значення перерахунку / декларації у файлах ініціалізації.


+1 Я згоден. Це пов'язано з Х - макросів . Я розглядаю це як спосіб запрограмувати препроцесор на написання частини мого коду, роблячи код ремонтопридатним. Є люди, які не згодні.
Mike Dunlavey
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.