Как создавать большие массивы программ и не раздражать компоновщика

У меня есть довольно сложный проект (PlatformIO, но у меня также было это в vanilla IDE) с несколькими вложенными включениями. Допустим, есть три файла и основной скетч. Основной скетч включает №2 и №3. Файл №3 включает файл №1 и файл №2.

Если файл №2 представляет собой файл .h, содержащий что-то вроде шрифта, в формате:

const char foo[] PROGMEM = {0xDE,0xAD,0xBE,0xEF};

Затем я получаю ошибки компоновщика несколько определений 'foo' как в #3, так и в main.

Как ни странно, шрифты по умолчанию, включенные в мою библиотеку OLED, помещаются в заголовочный файл, ТОЧНО так, как это сделал я, и все же несколько файлов, включающих эту библиотеку (или файлы, которые сами включали ее), не вызывают эту проблему с библиотекой. заголовочный файл.

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

Я использовал extern для решения этой проблемы, но это раздражает, и я не уверен, что это правильное поведение...

Может ли это быть как-то связано с #include <font.h> и #include "font.h"? В чем разница? В моей ссылке сказано, что они означают одно и то же, но теперь я не уверен.

, 👍0

Обсуждение

название вашего сообщения не соответствует тому, о чем ваш вопрос, @jsotola

Какое расширение у файла №3?, @Edgar Bonet

Это все файлы .h, кроме основного. Проблема на самом деле не связана с main — это происходит каждый раз, когда # 2 включается в несколько файлов. Другие файлы .h имеют связанный файл .cpp, за исключением #2., @RDragonrydr


1 ответ


Лучший ответ:

2

Защита включения защищает только от многократного включения файла заголовка в одну и ту же единицу перевода. Это важно. Если вы включаете заголовочный файл (1) в исходный файл, а затем включаете другой заголовочный файл (2) в тот же исходный файл, причем указанный заголовочный файл также включает предыдущий заголовочный файл (1), то защита включения выполнит свою функцию.

Если у вас есть два исходных файла и вы включаете один и тот же заголовок в оба, то защита включения ничего не сделает.

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

Определять переменные или функции в заголовочном файле следует только в том случае, если они являются типами простых констант, например const int или const char. - что-то, где компилятор может дословно заменить его в коде и полностью оптимизировать символ (массивы любого типа не являются простыми), или если символ объявлен как "статический", где он будет вынужден оставаться «внутри» единицы перевода и не будет экспортироваться для связывания.

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

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

extern const char foo[] PROGMEM;

И в исходном файле определите константу:

#include "foo.h"
const char foo[] PROGMEM = {0xDE,0xAD,0xBE,0xEF};

И в ответ на вопрос "в чем разница между #include <...> и #include "..." - когда речь идет о Arduino, ничего подобного.

Официально это означает разделение между "системными" и "локальными" заголовками. То есть заголовки, которые устанавливаются в ОС как часть компилятора, и заголовки, которые содержатся внутри проекта. Однако в случае с Arduino такого различия нет, и они означают одно и то же — просто заголовок.

И, наконец, примечание по терминологии — глоссарий, если хотите:

  • Единица перевода — один компилируемый исходный файл (и все включенные заголовки), формирующий единый файл объектного кода, который необходимо связать.
  • Объявление (Declare) — указание типа символа и любых атрибутов, но не содержимого этого символа.
  • Определение (Define) — связывание символа при его объявлении с содержимым этого символа.
  • Символ – имя любой переменной, функции, класса и т. д.
,

Итак, extern — правильный способ сделать это. Есть идеи, как OLED-библиотеке это удается? Он определяет И объявляет этот массив в своем собственном файле .h и не вызывает ошибок. И эта библиотека включается в ту, которую я использую в нескольких местах (хотя сама она включается только один раз...?), поэтому я _думаю_, что это должно вызвать ошибку..., @RDragonrydr

Можете ли вы указать на эту библиотеку OLED? Я никак не могу предсказать, что он делает, не видя его., @Majenko

https://github.com/ThingPulse/esp8266-oled-ssd1306/blob/master/src/OLEDDisplayFonts.h Однако это работает, этот файл включен в заголовок SSD1306Wire, который включен в заголовок SSD1306, который я использую., @RDragonrydr

Возможно, разные версии компилятора делают что-то по-разному. Я знаю, что недавно была добавлена дедупликация статических данных (таких как эти большие массивы) и что компилятор ESP8266 достаточно новый для этого — возможно, это избавило его от необходимости жаловаться на то, что он был там более одного раза, поскольку теперь он может знайте, что все они в любом случае являются одними и теми же данными., @Majenko

Странно, что это не помогло... что бы это ни было... для меня. Я все еще изучаю тонкости того, как файлы, заголовки и размещение в них вещей влияют на то, где они появляются, а затем происходит это, где, по крайней мере, часть ответа кажется «потому что он хочет»... : Д, @RDragonrydr