Как разбить программу на модули

По умолчанию проект Arduino состоит из единственного файла с расширением *.ino и одноименной папки. В файле *.ino находится весь основной код приложения:

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

Поделить проект Arduino на отдельные модули довольно просто. Достаточно в папке проекта создать файл с расширением имямодуля1.cpp и заголовочный файл имямодуля1.h, и перетащить туда из файла *.ino необходимый код. После этого система Arduino IDE будет автоматически компилировать код как из файла *.ino, так и из файла имямодуля1.cpp.

Предположим, у Вас есть файл *.ino, в котором находятся две функции чтения и записи микросхемы EEPROM. Убрать их код в отдельный модуль можно следующим способом, процесс по шагам:

1. Создайте в папке проекта текстовый файл для исходного кода с расширением *.cpp. Этому файлу можно дать произвольное имя. Основной принцип — имя должно быть понятным, не слишком длинным, и должно отражать назначение кода, который в нем находится. В нашем примере это код для чтения EEPROM, поэтому дадим ему имя EEPROMrw.cpp.

5. Язык Си: как разделить код на модули

2. Методом Cut/Paste перенесите код для EEPROM из файла *.ino в файл EEPROMrw.cpp.

В нашем примере файл EEPROMrw.cpp может содержать следующий код:

#include «EEPROMrw.h»
unsigned char ReadFrom24C04(unsigned char addr) < int data; Wire.beginTransmission(0x50); Wire.write(addr); Wire.endTransmission(); delay(5); Wire.requestFrom(0x50,1); delay(10); if(Wire.available()) < data = Wire.read(); > return data; >
void WriteTo24C04(unsigned char addr, unsigned char data) < Wire.beginTransmission(0x50); Wire.write(addr); Wire.write(data); Wire.endTransmission(); >

Обратите внимание, что в начале модуля есть строчка с оператором #include, она подключает заголовочный файл EEPROMrw.h. В этом заголовочном файле будут находиться прототипы функций модуля EEPROMrw.cpp.

3. Создайте новый файл с таким же именем, что и имя нового модуля, но с расширением *.h. Для нашего примера получится файл с именем EEPROMrw.h.

В файле EEPROMrw.h может содержаться такой текст:

#include // библиотека шины I2C для EEPROM 24LC04
unsigned char ReadFrom24C04(unsigned char addr);
void WriteTo24C04(unsigned char addr, unsigned char data);

В результате в папке проекта будет находиться ino-файл, и два новых файла EEPROMrw.cpp и EEPROMrw.h:

имяпроектаимяпроекта.ino
EEPROMrw.cpp
EEPROMrw.h

4. Добавьте в начало ino-файла оператор #include для подключения нашего заголовка:

#include «EEPROMrw.h»

5. Скомпилируйте проект (Ctrl+R). Система Arduino IDE обработает как код из файла имяпроекта.ino, так и из файла EEPROMrw.cpp.

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

Разбиваем проект на файлы

Такой способ организации кода в проекте дает нам следующие преимущества:

• Отлаженные куски кода можно прятать в отдельные файлы, в результате чего основной код в файле *.ino упрощается, в нем проще ориентироваться, работать становится намного легче. На аналогичном принципе основано использование библиотек Arduino (библиотеки Serial, Wire и т. д.).
• В отдельные модули и заголовки можно убрать глобальные переменные и определения #define, определения типов. Некоторые переменные можно сделать вовсе локальными для модуля (static). Это также облегчает понимание кода и его поддержку.

[Ссылки]

1. Arduino: что там внутри? (FAQ).
2. 220513usdx_rus.zip — проект SDR-трансивера uSDX, поделенный на модули.

Источник: microsin.net

13.6. Разбиение программы на модули.

Разбиение программы на модули (отдельные файлы с текстом программы) позволяет использовать готовые модули в разных программах, а также является важнейшим способом разделения труда при работе в коллективе.

При таком разбиении в одних модулях должны содержаться функции, а в других — их вызовы. Модули должны быть объединены в единый проект, включающий головной файл (например, Project1) и отдельные модули (например, Unit1 и Unit2).

Существуют разные способы взаимосвязи модулей в проекте. Одни из них основаны на директиве #include (см. ниже тему «Препроцессор»). Другие — на описании extern:

Читайте также:
Программа которая убирает надписи

Пример 2. Разбиение программы на модули с использованием класса памяти extern:

Основной файл проекта

char str[ ] = “Rezult = ”;

printf(“n %s %dn”, str, y);

extern char str[ ];

printf(“ %s %dn”, str, x);

printf(“ %s %dn”, str, z);

14. Структуры и объединения

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

14.1. Понятие структуры

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

Определение объектов типа структуры производится за два шага:

– декларация структурного типа данных, не приводящая к выделению участка памяти;

– определение структурных переменных объявленного структурного типа с выделением для них памяти.

14.2. Декларация структурного типа данных

Структурный тип данных задается в виде шаблона, общий формат описания которого следующий:

struct ID структурного типа

описание полей

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

Описание полей производится обычным способом: указываются типы и идентификаторы.

Пример определения структурного типа:

Необходимо создать шаблон, описывающий информацию о студенте: номер группы, ФИО и средний балл. Один из возможных вариантов:

Поля одного типа при описании можно объединять в одну группу:

char Number[10], Fio[40];

Размещение данного объекта типа Stud_type в ОП схематически будет выглядеть следующим образом:

Структурный тип данных удобно применять для групповой обработки логически связанных объектов.

Иногда параметрами таких операций могут выступать адрес и размер (либо тип) структуры. Примеры подобных групповых операций:

— захват и освобождение памяти для объекта;

— запись и чтение данных, хранящихся на внешних носителях как физические и/или логические записи с известной структурой (при работе с файлами).

Т.к. одним из параметров групповой обработки структурных объектов является размер, нужно быть осторожным, если декларировать поле структуры как объект переменной размерности (например, как тип String), т.к. в этом случае «переменная» его часть будет храниться отдельно от остальной структуры, и некоторые операции со структурны­ми данными будут не корректны, например:

String Number, fio;

Хотя само такое определение структуры вполне допустимо, но не все операции со структурами, описанные ниже (например, запись целой структуры в файл), в применении к нему дадут верный результат.

Некоторые особенности:

1) поля структуры, как правило, имеют разный тип, кроме функций, файлов и самой этой структуры;

2) поля не могут иметь атрибут, указывающий «класс памяти», данный атрибут можно определить только для всей структуры;

3) идентификаторы (ID) как самой структуры, так и ее полей могут совпадать с ID других объектов программы, т.к. шаблон структуры обладает собственным пространством имен;

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

Источник: studfile.net

Разделение на модули

Человеческий мозг плохо справляется с обработкой больших объемов неструктурированной информации. А программное обеспечение является примером сложной системы, причем сложность программных продуктов постоянно увеличивается. Википедия приводит данные о числе строк кода в Windows и Linux. Так, например, ядро Linux в 1991 году содержало 10 тысяч строк кода, в 1999 — миллион, а в 2017 — более 18 миллионов строк кода, то есть в 1000 раз больше, чем в первой версии. Возникает естественный вопрос: как люди справляются с написанием таких больших программ?

  • группы операторов объединяют в функции;
  • простые функции используются в более сложных функциях;
  • наборы функций объединяют в модули;
  • модули объединяют в подсистемы;
  • подсистемы в системы;
  • системы, в свою очередь, могут быть частями более крупных приложений.
  • Интерфейс определяет набор действий или функций, которые поддерживает модуль. Это определение того, что делает модуль.
  • Реализация модуля — это программный код, который реализует заявленные в интерфейсе функции.

Интерфейс позволяет скрыть сложные решения и использовать модуль как «черный ящик». Так, в известной системе символьных вычислений Wolfram Mathematica есть функция разложения числа на множители. С точки зрения пользователя функция FactorInteger выглядит предельно ясно: на вход подается целое число, на выходе получаем список его делителей. При этом разработчики системы хвалились (ссылка не источник не указана!), что эта функция работает очень эффективно, а реализация метода составляет более 500 страниц кода на Си (конечно, в этой браваде чувствуется рекламный подтекст: «Не пытайтесь повторить такое в домашних условиях! Покупайте наше»).

Читайте также:
Программы подготовки кадров это

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

Модули в языке Си

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

Пусть нам нужно вычислять определенный интеграл от произвольной функции действительного аргумента (хорошо-хорошо, значения типа double ), находить минимум функции и решать другие стандартные задачи численного анализа. Наша цель — написать модуль методов численного анализа. Назовем его numalgs (numerical algorithms). Начнем с написания интерфейса. Если мы не сможем понять что делать, то нечего браться за реализацию.

Интерфейс (заголовочный файл)

У модуля обычно есть один заголовочный файл, имя которого совпадает, или как-то связано, с название модуля. В нашем примере заголовочный файл будет назваться numalg.h. В этом файле мы объявляем тип данных для функций действительного аргумента, коды ошибок, которые могут возникать при выполнении наших численных методов, и прототипы «основных» функций.

/** Множество функций из R в R. */ typedef double (*RRFun)(double); /** Код ошибок функций. Префикс NA означает Numerical Algorithms. */ typedef enum < NA_OK = 0, /* Корректное выполнение */ NA_CONVERGE = 1, /* Не удалось вычислить значение с заданной точностью */ NA_PARAM = 2 /* Некорректное значение входных параметров вызова */ >NAError; /** ** Вычисление приближения определенного интеграла функции f на отрезке [a, b] с точностью epsilon. ** ** Параметры: ** f: подынтегральная функция. ** a, b: нижний и верхний предел интегрирования. ** epsilon: требуемая точность. ** err: адрес для сохранения кода возврата. ** ** Если передается ненулевое значение err, то по окончании функции по этому адресу записывается ** код ошибки.

Если err имеет значение NULL, то функция не записывает код возврата. ** ** Возвращаемое значение: ** В случае успеха функция возвращает значение интеграла, а по адресу err записывается код NA_OK. ** ** Если значение интеграла не удается получить с заданной точностью, то возвращается ** его приближение. В этом случае по адресу err записывается код NA_CONVERGE. ** ** В случае некорректных входных значений, a > b, нулевой адрес f, функция возвращает 0 ** и записывает по адресу err код NA_PARAM. */ double integrate(RRFun f, double a, double b, double epsilon, NAError *err); /** ** Поиск аргумента минимума функции f на отрезке [a, b]. ** ** . подробное описание . */ double minimize(RRFun f, double a, double b, double epsilon, NAError *err);

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

Что включать в заголовочный файл интерфейса модуля? В файле должны быть приведены прототипы всех общедоступных функций, и определены все типы, необходимые для успешной трансляции этого файла. Например, если одна из интерфейсных функций модуля получает на вход открытый файл, то есть FILE * , то необходимо включить заголовочный файл stdio.h .

Чего НЕ ДОЛЖНО быть в заголовочном файле? Там не должно быть ничего лишнего. Не следует в этом файле объявлять функции, которые нужны только для реализации интерфейсной функции, то есть не предполагается, что их будет использовать кто-то еще. Чем меньше пользователь будут знать о деталях реализации, тем лучше.

Не надо включать лишние заголовочные файлы «на всякий случай», даже если этот заголовочный файл потребуется в реализации. Включите этот заголовочный файл в том файле реализации, где он понадобится. Интерфейс должен быть в некотором смысле минимальным.

Читайте также:
Программа исследования и методы сбора информации

Реализация (.c-файлы)

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

Для реализации конкретного метода, например, численного интегрирования, могут понадобиться вспомогательные функции. Если интеграл вычисляется по составной квадратурной формуле, то может понадобиться вспомогательная функция, которая вычисляет значение квадратурной формулы на заданном отрезке, а разбиение отрезка [a, b] на части с шагом h выполняется в функции integrate .

Таким образом, файл с реализацией метода численного интегрирования, integrate.c, может содержать помимо основной функции integrate несколько дополнительных, которые не включены в интерфейс модуля. Вспомогательные функции, которые используются только в том файле, где они реализованы, помечаются статическими (ключевое слово static перед типом возвращаемого значения). Если имена «обычных» функций должны быть уникальными в рамках всей программы (поскольку их можно вызывать из других исходных файлов), то область видимости статической функции ограничена одним файлом. В разных файлах могут быть статические функции с совпадающими именами.

Все перечисленное приводит нас к приблизительно такому содержанию файла integrate.c.

#include «numalgs.h» /** Вычисляет значение интеграла от функции f на отрезке [a, b] методом Симпсона. */ static double simpson(RRFun f, double a, double b); /** Здесь дублируется описание из заголовочного файла, которое опущено в целях сокращения места. ** ** Метод: ** Используется квадратурная формула второго порядка с динамическим выбором шага по правилу Рунге. */ double integrate(RRFun f, double a, double b, double epsilon, NAError *err) < // Реализация функции >/** Вычисляет значение интеграла от функции f на отрезке [a, b] методом Симпсона. ** ** Функция f приближается на отрезке [a, b] параболой, построенной по значениям ** f в точках a, (a + b)/2, b. ** ** Предполагается, что a < b и точка (a + b)/2 лежит строго между a и b. */ static double simpson(RRFun f, double a, double b) < // Реализация функции >

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

Использование модуля в приложении

Отлично, мы написали модуль. А как им пользоваться?

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

#include #include «numalgs.h» int main()

Компилируем. Здесь предполагается, что файлы модуля (заголовочный и файлы реализации) находятся в той же директории, что и main.c.

gcc -c main.c gcc -c integrate.c gcc -o prog main.o integrate.o # Эквивалентная команда для «ленивых»: gcc main.c integrate.c

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

Проверка корректности входных данных

  • Функции, объявленные в интерфейсе, должны проверять все возможные ошибки.

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

  • Все предположения и требования к входным данным должны быть явно описаны в документации интерфейса.

Резюме

  • Модуль представляет собой решение некоторой логически замкнутой задачи или нескольких задач в одной области.
  • Интерфейс модуля определяет набор функций, доступных извне.
  • Интерфейс не содержит ничего лишнего.
  • Документация интерфейсных функций должна максимально подробно описывать их поведение.
  • Реализации обвяленных в интерфейсе функций должны проверять все возможные ошибки входных данных.

Источник: serg.tk

Рейтинг
( Пока оценок нет )
Загрузка ...
EFT-Soft.ru