Как создать свою библиотеку в c
Перейти к содержимому

Как создать свою библиотеку в c

  • автор:

Как написать свою библиотеку на Си

Если ты встал на путь С/С++ разработчика, то скорее всего (помимо использования стандартной библиотеки — libc) рано или поздно вам потребуется занятся разработкой собственных библиотек. Зачем. Причин может быть несколько. Например вы написали свою структуру данных или свой алгоритм, и хотите использовать его повторно или распространять. Так же возможно вы написали несколько утилит и все они используют один и тот же кусок кода (например, как часто это бывает, логгер), и будет логично вынести этот кусок кода в отдельный модуль. Поскольку сопровождать такой код будет проще.

Реализация

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

1 2 3 4 5 6
#ifndef __SOMECODE__ #define __SOMECODE__ void print_split(char* str); #endif 

И создадим файл somecode.c, в которой напишем реализацию нашей функции.

1 2 3 4 5 6 7 8 9 10 11 12
#include "somecode.h" #include  #include void print_split(char* str)  const char *word = strtok(str, " "); while (word != NULL)  printf("> %s\n", word); word = strtok(NULL, " "); > > 

Далее создадим файл main.c, где будем вызывать нашу функцию.

1 2 3 4 5 6 7 8 9 10
#include  #include "somecode.h" int main(int argc, char** argv)  if (argc > 1)  print_split(argv[1]); > > 

Давайте для начала скомпилируем это все самым обычным способом для проверки работоспособности.

  • из исходных файлов получаем объектные файлы
  • из объектных файлов получаем исполняемый файл
1 2 3 4 5 6 7 8 9 10 11 12
$ gcc -c -Wall -g -o somecode.o somecode.c $ gcc -c -Wall -g -o main.o main.c $ gcc -Wall -g -o a.out.1 main.o somecode.o $ ls -l $ ls -l total 40 -rwxr-xr-x 1 ainr ainr 19816 Aug 9 21:22 a.out.1 -rw-r--r-- 1 ainr ainr 123 Aug 9 21:20 main.c -rw-r--r-- 1 ainr ainr 3576 Aug 9 21:22 main.o -rw-r--r-- 1 ainr ainr 213 Aug 9 21:21 somecode.c -rw-r--r-- 1 ainr ainr 72 Aug 9 21:20 somecode.h -rw-r--r-- 1 ainr ainr 6224 Aug 9 21:21 somecode.o

Запускаем исполняемый файл и видим, что все работает.

1 2 3 4 5
$ ./a.out.1 "hello my little pony" > hello > my > little > pony

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

$ gcc -Wall -g -shared -fpic -o libsomecode.so somecode.c

Из файла с расширением .c мы получаем файл с расширением .so. И так же обратите внимание, что библиотека имеет префикс lib. Еще мы видим, что появились два дополнительных аргумента -shared и -fpic. С помощью опции -shared мы говорим компилятору, что хотим получить а выходе библиотеку. А опция -fpic говорит компилятору, что объектные файлы должны содержать позиционно-независимый код (position independent code), который рекомендуется использовать для динамических библиотек.

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

1 2 3
$ gcc -Wall -g -o a.out.2 main.c -lsomecode /usr/bin/ld: невозможно найти -lsomecode collect2: error: ld returned 1 exit status

И опс.. мы получили ошибку… Линкер говорит нам, что он не знает где лежит наша библиотека. С помощью опции -L указываем текущую директорию, где лежит наша библиотека и компиляция проходит успешно.

$ gcc -Wall -g -o a.out.2 main.c -lsomecode -L. 

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

$ ./a.out.2 "hello my little pony" ./a.out.2: error while loading shared libraries: libsomecode.so: cannot open shared object file: No such file or directory

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

1 2 3 4 5 6 7 8 9 10 11 12 13
$ ld --verbose | grep SEARCH_DIR | sed "s/\;\ /\n/g" SEARCH_DIR("=/usr/local/lib/x86_64-linux-gnu") SEARCH_DIR("=/lib/x86_64-linux-gnu") SEARCH_DIR("=/usr/lib/x86_64-linux-gnu") SEARCH_DIR("=/usr/lib/x86_64-linux-gnu64") SEARCH_DIR("=/usr/local/lib64") SEARCH_DIR("=/lib64") SEARCH_DIR("=/usr/lib64") SEARCH_DIR("=/usr/local/lib") SEARCH_DIR("=/lib") SEARCH_DIR("=/usr/lib") SEARCH_DIR("=/usr/x86_64-linux-gnu/lib64") SEARCH_DIR("=/usr/x86_64-linux-gnu/lib"); 

Так же есть возможность задавать дополнительные директории с библиотеками с помощью переменной окружения LD_LIBRARY_PATH. С помощью утилиты ldd посмотрим от каких библиотек зависит наша программа.

1 2 3 4 5
$ ldd a.out.2 linux-vdso.so.1 (0x00007ffffe4e7000) libsomecode.so => not found libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fcf3b680000) /lib64/ld-linux-x86-64.so.2 (0x00007fcf3b897000) 

Добавим в LD_LIBRARY_PATH текущую директорию.

$ export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$PWD" 

Видим, что наша библиотека подгрузилась.

1 2 3 4 5
ldd a.out.2 linux-vdso.so.1 (0x00007fffe2115000) libsomecode.so (0x00007f4848a6c000) libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4848860000) /lib64/ld-linux-x86-64.so.2 (0x00007f4848a76000) 

И теперь программа запускается и работает.

1 2 3 4 5
$ ./a.out.2 "hello my little pony" > hello > my > little > pony

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

1 2 3 4 5 6 7 8 9 10
$ ls -l total 92 -rwxr-xr-x 1 ainr ainr 19816 Aug 9 21:22 a.out.1 -rwxr-xr-x 1 ainr ainr 17864 Aug 9 21:25 a.out.2 -rwxr-xr-x 1 ainr ainr 18808 Aug 9 21:24 libsomecode.so -rw-r--r-- 1 ainr ainr 123 Aug 9 21:20 main.c -rw-r--r-- 1 ainr ainr 3576 Aug 9 21:23 main.o -rw-r--r-- 1 ainr ainr 213 Aug 9 21:21 somecode.c -rw-r--r-- 1 ainr ainr 72 Aug 9 21:20 somecode.h -rw-r--r-- 1 ainr ainr 6224 Aug 9 21:23 somecode.o

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

1 2 3 4 5 6
$ objdump -t a.out.1 | grep print_split 000000000000119c g F .text 0000000000000061 print_split $ objdump -t a.out.2 | grep print_split 0000000000000000 F *UND* 0000000000000000 print_split $ objdump -t libsomecode.so | grep print_split 0000000000001139 g F .text 0000000000000061 print_split

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

Так что же мы сделали?

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

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
-------------------------------- --------------------------------- | Заголовочный файл | | Реализация | | (somecode.h) | | (somecode.c) | -------------------------------- --------------------------------- | | | | | void print_split(char* string);| | void print_split(char* string) | | | | < | | | | // . | | | | >| | | | | ------------------------------- --------------------------------- | | Компилируем \ / (gcc -shared -fpic -o liblog.so . ) | --------------------------------- | Динамическая библиотека | | (liblog.so) | --------------------------------- | | (gcc . -llog) --------------------------------------------- | | | ----------------------- ----------------------- ----------------------- | Утилита1 | | Утилита2 | | Утилита3 | ----------------------- ----------------------- ----------------------- | | | | | | | #include "somecode.h" | | #include "somecode.h" | | #include "somecode.h" | | | | | | | | print_split(". "); | | print_split(". "); | | print_split(". "); | | | | | | | ----------------------- ----------------------- -----------------------

Примерно те же действия вы будете делать и под windows и под мак, будут немного другие компиляторы, но идея одна.

Создание статической библиотеки на С++ для работы с большими числами

Я всегда слышал, что с библиотеками в С++ что-то не так, как и с ограничением максимального целочисленного значения, да и вообще то, что язык сложный и непонятный. Что же, сегодня, мы начнём писать собственную библиотеку больших чисел, полностью своими руками c 0, и узнаем, так ли страшен С++, как его малюют?

Если вы не разбираетесь в С++, не переживайте, эта статья имеет нулевой порог вхождения. Мы начнём с лёгкого, но вы даже не заметите, как начнёте разбираться в более сложных и непонятных, на первый взгляд, вещах. Главное, писать код логично. Думаю, данная статья будет интересна не только начинающим, ведь я постарался затронуть достаточно много тем. (для старожилов: моя цель не сделать оптимизирование или быстрее, а показать, что С++ не такой уж и сложный язык программирования. И да, я знаю, что существуют другие библиотеки, которые делают это быстрее и лучше. И да, было бы круче, если бы мы использовали булевую алгебру. И да, С++ про вечную оптимизацию, но это статья не про это. Спасибо.)

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

▍ Предисловие и планы

С++ строготипизированный язык программирования, где максимально возможное значение целочисленной переменной, является максимальное значение unsigned long long int (где-то 18 446 744 073 709 551 615). Этого бывает недостаточно, поэтому я решил разработать собственную библиотеку больших чисел (BigNumLib). Единственное ограничение размерности BigNumLib переменной – это количество цифр, из которого будет состоять число, то есть, максимально в число поместится 4 294 967 295 цифр.

▍ Начало разработки

Итак, начинаем разработку! Для начала нам необходимо продумать логику и возможности нашего собственного типа данных. Как мы создадим свой тип данных? В ЯП С++ нельзя расширить стандартные типы (int, double и т.п.), поэтому, единственный возможный вариант, который у нас остался, это работа через struct и class.

Чем отличаются Struct и Class?

Ответ: единственное различие между ними, так это то, что в struct модификаторы доступа по умолчанию public, а в class — private. Также отличается и наследование по умолчанию.

Итак, откроем Visual Studio с пустым проектом. Создадим папки (если они не созданы): “Файлы заголовков” с файлом BigNumLib.h и “Исходные файлы” с файлами Main.cpp, BigNumLib.cpp. У кого трудности на данном этапе, ничего страшного, ниже представлены фото и gif.

Как создать и настроить проект

image

image

В файле заголовка (.h), можно заметить строчку #pragma once. Что это?

Ответ: В языках программирования С и C++ #pragma once — нестандартная, но широко распространённая препроцессорная директива, разработанная для контроля за тем, чтобы конкретный исходный файл при компиляции подключался строго один раз.

Здесь, в этом файле заголовка, мы будем создавать прототипы функций (функции без тела) и вообще описывать весь класс, например, какие дополнительные библиотеки будут подключены, для исправной работы нашей, или какие функции будут доступны пользователю, а какие нет (модификаторы доступа public, private). Зачем мы вообще создали данный заголовок? Всё просто, чтобы подключить внешний код, необходимо использовать именно заголовок.

▍ Создание bignum класса

Итак, для начала нам необходимо создать класс и его поля:

class bignum

Ловим ошибку, что не подключили библиотеку и подключаем:

#include

Итак, что мы написали?

_value = здесь будет храниться наше число в виде строки

_size = из скольких цифр состоит наше число (размер). size_t это псевдоним, то есть, то же самое что и unsigned int (положительные целые числа)

_isNegative = является ли отрицательным числом. (true или false)

bignum() = конструктор класса. Он вызывается при создании экземпляра класса.

private: – поле, где доступ к данным имеет лишь класс. Приватные переменные, как правило, пишутся через ‘_’.

Так отлично, теперь откроем BigNumLib.cpp и напишем там такой код:

#include "bignum.h" bignum::bignum()

Здесь мы подключили наш заголовок и описали конструктор класса, где доступ к конструктору мы получаем через пространство имён ( bignum:: )

Теперь мы можем открыть наш основной файл ( Main.cpp ) и проверить работу библиотеки:

#include "BigNumLib.h" int main()

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

Что же, теперь ставим точку остановы на return 0 , и смотрим нашу переменную.

Отлично, всё работает! По поводу отладчика, это безумно удобный интерфейс. Как сказал один мудрый человек, если программист не умеет пользоваться отладчиком, то этот человек не программист. Краткий экскурс по данному чуду: f5 (запуск отладки), shift+f5 (остановка), ctrl+shift+f5 (перезапуск), f10 (шаг с заходом в функцию), f11 (шаг с обходом функции), shift+f11 (шаг с выходом из функции), f5 (во время отладки, перейти к следующей точке остановы).

▍ Геттеры

Так, теперь создадим функцию геттер, чтобы иметь возможность читать поле нашего класса. Для этого объявим эту функцию в BigNumLib.h (файл заголовок) в поле public :

//@return string std::string getValue();

Мы написали комментарии в стиле DOC++. Этот тип комментариев понимает Visual Studio и красиво отображает нам. (ключевые слова пишутся через ‘@’: @return, @param )

image

Теперь необходимо прописать логику данных функций в BigNumLib.cpp .

std::string bignum::getValue() < std::string _value = this->_value; if (this->_isNegative) _value.insert(0, "-"); return _value; >

Чтобы обратиться к внутренним полям класса, мы используем указатель this , таким образом, пользователь имеет доступ к переменной _value только на чтение.

В функции getValue() мы создаём локальную переменную _value и заполняем её данными лежащими в поле класса. Если поле _isNegative имеет значение true (число отрицательно), то мы вставляем в начало строки ‘-’. (insert(позиция, знак))

Отлично, теперь проверим наш код в действии!

#include "BigNumLib.h" #include int main() < bignum a; std::cout  

▍ Перегрузка конструктора (приём int)


Далее нам необходимо создать перегрузку конструктора класса, который на вход принимает long long int. Для этого объявим:

BigNumLib.h

public: bignum(long long int other_value);

BigNumLib.cpp

bignum::bignum(long long other_value)

Перегрузка конструктора \ функции \ оператора – это когда они имеют одно и то же имя, но принимают разные параметры. Та или иная функция вызывается в зависимости от принимаемых ею аргументов.

Здесь, мы впервые использовали тернарный оператор. Сокращённое написание конструкции if, else . Всё предельно просто, если на вход поступает отрицательное число, то поле _isNegative становится true . После чего, число переводится в строку и если число отрицательное, то удаляется первый символ из строки (‘-’).

Сейчас мы можем протестировать это и присвоить число. Попробуем присвоить положительное и отрицательное число, посмотрим, правильно ли работает наша программа:

image

Отлично, теперь попробуем ввести огромное число:

image

Как мы можем видеть, Visual Studio запрещает нам присваивать такое огромное число, потому что оно выходит за рамки long long int (превысив значение самого большого стандартного типа). Как мы будем обходить данный запрет? С помощью строки, ведь она, практически безгранична.

▍ Перегрузка конструктора (приём string, char*)

Создадим конструктор и пару функций в файле заголовка.

public: bignum(const char* other_value); bignum(std::string other_value); private: void parsStringToBigNumParams();

BigNumLib.cpp

bignum::bignum(const char* other_value) < _value = other_value; parsStringToBigNumParams(); >bignum::bignum(std::string other_value) < _value = other_value; parsStringToBigNumParams(); >void bignum::parsStringToBigNumParams() < if (_value[0] == '-') < _value = _value.erase(0, 1); _isNegative = true; >else _isNegative = false; if (_value.find_first_not_of("0123456789") != std::string::npos) throw std::runtime_error(_value + " it's not a number!"); _size = _value.size(); >

Мы написали 2 конструктора, где один из них принимает string, а другой массив char. Зачем? Потому что в случае, когда после присвоения сразу записывается значение, то будет массив char. А если создать string переменную и присвоить уже её, то активируется другой конструктор (со string параметром).

image

По поводу функции parsStringToBigNumParams() . Данная функция превращает строку, в набор параметров нашего класса. В начале она проверяет, стоит ли ‘-’, на первой позиции в _value , если да, убрать знак из строки и присвоить параметру _isNegative = true . После чего идёт проверка, если в _value найдено, что какой-то элемент не совпадает с цифирным набором (npos — не найдено совпадений), то выкинуть исключение. И дальше присвоить размер.

Что делает исключение? Полностью останавливает работу программы, если его не обработать. Давайте же опробуем работу наших конструкторов и обработаем исключение:

image

Как работает обработка исключений? В блоке try пишется небезопасный код, если в нём происходит ошибка, мы тут же попадаем в блок catch . Из-за того, что мы добавили описание в нашу самодельную ошибку, мы можем увидеть, какое значение, смогло сломать нашу библиотеку.

▍ Заключение

Что же, я чувствую, что выдал достаточно много инфы и, если продолжить, у новичков она может превратиться в кашу. За сегодня мы прошли очень много интересных тем и познакомились с некоторыми особенностями языка С++, но впереди ещё больше крутой информации, такая как перегрузка операторов, указатели, resize string и собственная логика в математических операциях. Если вам заходит такой формат обучения/разработки реального проекта, дайте знать, буду пилить 2 часть в таком же формате, ну, если вы вообще ждёте 2 часть 🙂

  • c++
  • c++ библиотеки
  • visual c++
  • visual studio
  • создание библиотек
  • написание библиотек
  • большие числа
  • большие данные
  • ruvds_статьи
  • Блог компании RUVDS.com
  • C++
  • Visual Studio

Как создать свою библиотеку?

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

Как написать свою библиотеку функций?
Я только начинаю изучать программирование на С++, параллельно разбираюсь с ООП. Вообще идеи ООП.

Как правильно подключить свою библиотеку?
Здравствуйте всем. Я использую Visual Studio. В одном решении у меня находится проект библиотеки.

Как добавить свою библиотеку в стандартные С++
Доброго времени суток, уважаемые форумчане! Недавно стало интересно, можно ли добавить свою.

Эксперт С++

8049 / 4806 / 655
Регистрация: 24.06.2010
Сообщений: 10,562

Библиотеку именно? .dll или .h? Если .h то создаешь Header файл. И пишешь туда функции и все что тебе нужно (в разумных пределах, не забывая про правила). В остальных файлах где нужно подключаешь этот файл, примерно так: #include "file.h";

Мат в 32 хода
237 / 172 / 18
Регистрация: 10.09.2009
Сообщений: 1,096

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

1 2 3 4 5 6
#ifndef _fileName_H_ #define _fileName_H_ . //функции, . //функции, . //функции #endif;

после этого, сохранищь фаил с расширением .h
а в клиентском фаиле пропишеш -

#include "fileName"

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

Пишем свою библиотеку

В этом уроке мы научимся писать собственные библиотеки для Arduino и разберём некоторые типовые вопросы по взаимодействию кода в библиотеке и кода в скетче (в главном файле программы). Это третий урок, который относится к библиотекам: обязательно прочитайте и усвойте урок про объекты и классы из блока программирования, и урок по использованию библиотек из блока базовых уроков, а также урок про создание функций. В этом уроке мы будем использовать все наши предыдущие знания, так что рекомендую разобраться со всем, что было непонятно. Писать библиотеки очень удобно в текстовом редакторе Notepad++ (официальный сайт) – так называемом блокноте программиста. Данный блокнот распознаёт и подсвечивает синтаксис, умеет в автодополнение текста и расширенный поиск, и многое многое другое. Безумно рекомендую работать именно в нём, если вы не умеете пользоваться Microsoft Visual Studio и прочими серьёзными средами разработки. Также рекомендую к прочтению вот этот урок с сайта Arduino.ru, в нём кратко пошагово рассказывают о создании библиотеки без излишеств. Если будете компилировать пример из этой статьи – замените WProgram.h на Arduino.h.

Разбираемся с файлами

Библиотека – это в первую очередь текстовый файл с кодом, который мы можем подключить в свой скетч и использовать имеющиеся там команды. Библиотека может иметь несколько файлов или даже папок с файлами, но подключается всегда один – главный заголовочный файл с расширением .h, а он в свою очередь подтягивает остальные необходимые файлы. В общем случае библиотека имеет такую структуру (название библиотеки testLib):

  • testLib – папка библиотеки
    • examples – папка с примерами
    • testLib.h – заголовочный файл
    • testLib.cpp – файл реализации
    • keywords.txt – карта подсветки синтаксиса

    Иногда файлы .h и .cpp могут находиться в папке src. Все файлы и папки, кроме заголовочного .h, являются необязательными и могут отсутствовать, т.е. библиотека может состоять только из заголовочного файла. В таком виде библиотека лежит в папке со всеми остальными библиотеками и может быть подключена в скетч при помощи команды #include. Вообще есть два места, где программа будет искать библиотеку (именно файл библиотеки):

    • Папка со скетчем
    • Папка с библиотеками

    Соответственно команда include имеет два варианта поиска файла, название заключается в <> или “”:

    • #include – будет искать файл в папке с библиотеками
    • #include “файл.h” – попробует найти файл в папке со скетчем, если не найдёт – пойдёт искать в папку с библиотеками

    Выглядит это вот так:

    Основа библиотеки

    Давайте заполним наш файл testLib.h, нашу тестовую библиотеку, минимальным кодом для работы:

    #ifndef testLib_h #define testLib_h #include // код библиотеки #endif

    Конструкция из директив препроцессора запрещает повторное подключение библиотеки и в целом является необязательной, но лучше не лениться и писать так. Файл библиотеки testLib.h находится в папке testLib в папке со всеми остальными библиотеками. Также мы подключаем основной файл Arduino.h для использования ардуино-функций в своём коде. Если таковых нет – его можно не подключать. Также подключаем testLib.h в наш тестовый скетч, как на скриншоте в прошлой главе. Конструкцию с #ifndef-define вы найдёте практически во всех библиотеках. На текущих версиях IDE (и, соответственно версии компилятора) можно делать так:

    #pragma once // подключаем Ардуино.н // код библиотеки

    Конструкция pragma once говорит компилятору, что данный файл нужно подключить только один раз, это просто короткая альтернатива #ifndef-define. Дальше будем использовать её

    Пишем класс

    Давайте воспользуемся наработками из урока объекты и классы и вставим финальную версию класса в testLib.h

    #pragma once #include // описание класса class Color < // класс Color public: Color(byte color = 5, byte bright = 30); void setColor(byte color); void setBright(byte bright); byte getColor(); byte getBright(); private: byte _color; // переменная цвета byte _bright; // переменная яркости >; // реализация методов Color::Color(byte color = 5, byte bright = 30) < // конструктор _color = color; // запоминаем _bright = bright; >void Color::setColor(byte color) void Color::setBright(byte bright) byte Color::getColor() byte Color::getBright()
    testSketch.ino
    #include Color myColor(10); // создаём объект myColor, указав _color (получим 10, 30) Color myColor2(10, 20); // указываем цвет и яркость! (получим 10, 20) Color myColor3; // без инициализации (получим 5, 30) void setup() < >void loop()

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

    testSketch.ino
    #include Color myColor(10); // создаём объект myColor, указав _color (получим 10, 30) Color myColor2(10, 20); // указываем цвет и яркость! (получим 10, 20) Color myColor3; // без инициализации (получим 5, 30) void setup() < Serial.begin(9600); Serial.println(myColor.getColor()); // 10 Serial.println(myColor2.getBright()); // 20 Serial.println(myColor3.getColor()); // 5 >void loop()

    Код выводит значения из класса, выводит правильно. Собственно вот мы и написали свою библиотеку! Далее можно разделить описание и реализацию, создав файл testLib.cpp

    #pragma once #include // описание класса class Color < // класс Color public: Color(byte color = 5, byte bright = 30); void setColor(byte color); void setBright(byte bright); byte getColor(); byte getBright(); private: byte _color; // переменная цвета byte _bright; // переменная яркости >;

    testLib.cpp
    #include // подключаем заголовок обязательно // реализация методов Color::Color(byte color = 5, byte bright = 30) < // конструктор _color = color; // запоминаем _bright = bright; >void Color::setColor(byte color) void Color::setBright(byte bright) byte Color::getColor() byte Color::getBright()
    testSketch.ino
    #include Color myColor(10); // создаём объект myColor, указав _color (получим 10, 30) Color myColor2(10, 20); // указываем цвет и яркость! (получим 10, 20) Color myColor3; // без инициализации (получим 5, 30) void setup() < Serial.begin(9600); Serial.println(myColor.getColor()); // 10 Serial.println(myColor2.getBright()); // 20 Serial.println(myColor3.getColor()); // 5 >void loop()

    Важный момент: если в библиотеке есть файл имябиблиотеки.cpp, то реализация методов и функций должна находиться именно там! В файле имябиблиотеки.h реализацию указывать нельзя, будет ошибка.

    Если библиотека состоит только из заголовочного файла имябиблиотеки.h , то реализацию можно расписать в нём.

    И вот уже у нас полноценная взрослая библиотека, разбитая на файлы. Можно дополнить её файлом keywords.txt, чтобы наши методы подсвечивались в коде.

    Keywords.txt

    keywords.txt это файл, в котором содержится “карта” подсветки синтаксиса, то есть каким цветом какие слова подсвечивать. Синтаксис построения этого файла очень прост: с новой строки перечисляются названия функций/методов, и через табуляцию (нажатие клавиши TAB) – тип ключевого слова.

    • KEYWORD1 – жирный оранжевый, подсветка для типов данных и названий классов
    • KEYWORD2 – оранжевый цвет, для методов и функций
    • LITERAL1 – голубой цвет, для констант

    Вот так будет выглядеть keywords.txt для нашей библиотеки:

    # комментарий testLib KEYWORD1 Color KEYWORD1 setColor KEYWORD2 setBright KEYWORD2 getColor KEYWORD2 getBright KEYWORD2

    Можно оставлять комментарии, здесь они начинаются с решётки #. Констант у нас нет, поэтому LITERAL1 не использовал. Давайте посмотрим, как выглядит код с подсветкой наших команд из библиотеки. Важный момент: чтобы изменения вступили в силу, нужно закрыть все окна Arduino IDE и открыть скетч заново. Почему Color не выделен жирным, да и вообще уже выделен в скетче без подсветки? Дело в том, что Arduino IDE собирает keywords из всех библиотек, и где-то имя Color видимо уже используется. Собственно вот и всё!

    Примеры реализации

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

    Библиотека без класса

    В библиотеке необязательно должен быть класс, может быть просто набор функций:

    #pragma once #include void printLol()
    testSketch.ino
    #include void setup() < Serial.begin(9600); printLol(); // выведет lol >void loop()

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

    #pragma once #include // объявление void printLol();

    testLib.cpp
    #include // подключаем заголовок обязательно // реализация void printLol()
    testSketch.ino
    #include void setup() < Serial.begin(9600); printLol(); // выведет lol >void loop()

    Обернём в namespace

    Данный пример относится к примеру выше: в “библиотеке” мы создали функции, имена этих функций могут совпасть с другими функциями в скетче, что приведёт к проблемам. Вместо написания класса, функции можно обернуть в “пространство имён” – namespace. Смотрите пример, я думаю всё станет понятно.

    #pragma once #include // пространство имён myFunc namespace myFunc < void printLol(); >; // реализация void myFunc::printLol()
    testSketch.ino
    #include void setup() < Serial.begin(9600); // выведет kek из функции скетча printLol(); // выведет lol из функции библиотеки myFunc::printLol(); >void printLol() < Serial.println("kek"); >void loop()

    Использование namespace позволяет разделить функции с одинаковыми названиями из разных документов, обращение к функции из пространства имён выглядит точно так же, как к классу: имяПространстваИмён::имяФункции .

    Передача и вывод значения в класс

    Рассмотрим такой пример: нужно передать в класс некое значение, обработать его и вернуть результат обратно в скетч. В качестве примера просто вернём умноженное на 10 число:

    #pragma once #include class testClass < public: long get_x10(int value); private: >;

    testLib.cpp
    #include // подключаем заголовок обязательно long testClass::get_x10(int value)
    testSketch.ino
    #include testClass testObject; void setup() < Serial.begin(9600); Serial.println(testObject.get_x10(450)); // выведет 4500 >void loop()

    Рассмотрим более сложную ситуацию: нужно принять значение в класс, записать в приватную переменную, и отдельным методом получить её:

    #pragma once #include class testClass < public: void setValue(int val); int getValue(); private: int _value = 0; >;

    testLib.cpp

    #include // подключаем заголовок обязательно void testClass::setValue(int val) < // берём внешнюю val и пишем в свою _value _value = val; >int testClass::getValue() < return _value; // вернуть переменную из класса >

    testSketch.ino
    #include testClass testObject; void setup() < Serial.begin(9600); testObject.setValue(666); Serial.println(testObject.getValue()); // выведет 666 >void loop()

    Изменение переменной из класса

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

    #pragma once #include class testClass < public: int multTo5(int value); void multTo7(int* value); private: >;

    testLib.cpp

    #include // подключаем заголовок обязательно int testClass::multTo5(int value) < // вернуть значение, умноженное на 5 return value * 5; >void testClass::multTo7(int* value) < // умножить переменную на 7 *value = *value * 7; >

    testSketch.ino
    #include testClass testObject; void setup() < int a = 10; a = testObject.multTo5(a); // a == 50 testObject.multTo7(&a); // a == 350 >void loop()

    В первом варианте мы передаём значение переменной, внутри метода умножаем его на 5 и возвращаем обратно, и можем приравнять эту же переменную в скетче к новому значению. В случае с указателем всё работает более интересно: мы передаём методу адрес переменной, умножаем эту переменную на 7 внутри класса, и всё. Грубо говоря, в этом примере *value является куклой вуду для переменной a: что мы будем делать с *value внутри метода – это сразу же будет отражаться на a. Данную тему можно развить до такого варианта: мы можем хранить в классе адрес переменной, и класс всегда будет иметь прямой доступ к значению переменной, его не нужно будет передавать каждый раз!

    #pragma once #include class testClass < public: void takeControl(int* value); void multTo6(); private: int *_value; // храним адрес >;

    testLib.cpp
    #include // подключаем заголовок обязательно void testClass::takeControl(int* value) < _value = value; >void testClass::multTo6()
    testSketch.ino
    #include testClass testObject; int a = 10; void setup() < // передали адрес a testObject.takeControl(&a); // сейчас а == 10 testObject.multTo6(); // тут а станет 60 a = 5; testObject.multTo6(); // тут а станет 30 testObject.multTo6(); // тут а станет 180 >void loop()
    Таким образом класс и его методы могут иметь полный контроль над переменной в основной программе!

    Передача массива в класс

    Попробуем передать массив в класс, чтобы методами класса можно было, например, сложить сумму элементов массива и вернуть её!

    #pragma once #include class testClass < public: long getSum(int *array, byte length); private: >;

    testLib.cpp

    #include // подключаем заголовок обязательно long testClass::getSum(int *array, byte length) < long sum = 0; // вычисляем длину массива length = length / sizeof(int); for (byte i = 0; i < length; i++) < sum += array[i]; >return sum; >

    testSketch.ino
    #include testClass testObject; void setup() < // делаем массив int myArray[] = ; // передаём массив и его размер (в байтах) long arraySum = testObject.getSum((int*)myArray, sizeof(myArray)); // arraySum == 438 > void loop()
    Основной механизм я думаю понятен, оставлю тут ещё пример, как передать структуру
    Передача структуры по указателю

    // передаем структуру по указателю struct foo_param_t < float *u; int n; float b; float c; >void foo(foo_param_t *p) < for (int i=0; in; i++) < float x = i*M_PI; p->u[i] = 1.0+p->b*x+p->c*x*x; > > void bar() < const int N = 10; float a[N]; foo_param_t p = ; foo(&p); >

    Передача структуры по ссылке

    // передаем структуру по ссылке struct foo_param_t < float *u; int n; float c; float b; >void foo(foo_param_t& p) < for (int i=0; i> void bar() < const int N = 10; float a[N]; foo_param_t p = ; foo(p); >

    Передача функции в класс

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

    #pragma once #include // тут хранится приаттааченная функция void (*atatchedF)(); // подключаем функцию void attachFunction(void (*function)()) < atatchedF = *function; >// вызов приаттаченной функции void callFunction()
    testSketch.ino
    #include void setup() < Serial.begin(9600); // подключили функцию printKek attachFunction(printKek); // вызвали подключенную функцию callFunction(); // вызовет printKek >void printKek() < Serial.println("kek"); >void loop()

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

    #pragma once #include class testClass < public: void attachFunction(void (*function)()); void callFunction(); private: void (*atatchedF)(); >;

    testLib.cpp
    #include // подключаем заголовок обязательно void testClass::attachFunction(void (*function)()) < atatchedF = *function; >void testClass::callFunction()
    testSketch.ino
    #include testClass testObj; void setup() < Serial.begin(9600); // подключили функцию printKek testObj.attachFunction(printKek); // вызвали подключенную функцию testObj.callFunction(); // вызовет printKek >void printKek() < Serial.println("kek"); >void loop()
    Второй вариант:

    #pragma once #include extern "C" < typedef void (*func)(void); >class testClass < public: void attachFunction(func newFunc); void callFunction(); private: func _attachedFunc; >;

    testLib.cpp
    #include // подключаем заголовок обязательно void testClass::attachFunction(func newFunc) < _attachedFunc = newFunc; >void testClass::callFunction()
    testSketch.ino
    #include testClass testObj; void setup() < Serial.begin(9600); // подключили функцию printKek testObj.attachFunction(printKek); // вызвали подключенную функцию testObj.callFunction(); // вызовет printKek >void printKek() < Serial.println("kek"); >void loop()

    Автоматическое создание объекта

    Создание класса подразумевает также создание объекта, но иногда библиотека пишется только для одного объекта (например – библиотека для работы с одним интерфейсом), и создание объекта в скетче выглядит как лишний код. Но, если вы откроете любой пример с использованием библиотеки Wire.h, вы не найдёте там создания объекта Wire, а он используется! Например:

    #include void setup() < Wire.begin(); >// .

    Мы используем объект Wire, но мы его не создавали! Иногда это может быть удобно, давайте покажу, как это сделать: нужно всего лишь добавить в заголовочный файл строчку:

    extern имя_класса имя_объекта;

    А в .cpp, если он есть, добавить:

    имя_класса имя_объекта = имя_класса();

    Таким образом объект будет создан внутри библиотеки, и мы сможем им пользоваться из скетча. Давайте возьмём самый первый пример из урока, из главы “Передача и вывод значения в класс”, и избавимся от лишнего создания объекта:

    #pragma once #include class testClass < public: long get_x10(int value); private: >; extern testClass testObject;

    testLib.cpp

    #include // подключаем заголовок обязательно long testClass::get_x10(int value) < return value*10; >testClass testObject = testClass();

    testSketch.ino
    #include // объект не создаём! void setup() < Serial.begin(9600); Serial.println(testObject.get_x10(450)); // выведет 4500 >void loop()

    Задание размера массива при создании объекта

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

    #define ARRAY_LEN 20 class myClass < public: byte vals[ARRAY_LEN]; private: >; myClass obj1; // тут у obj1.vals будет 20 ячеек myClass obj2; // тут у obj2.vals будет 20 ячеек myClass obj3; // тут у obj3.vals будет 20 ячеек

    Если мы хотим иметь возможность задать размер массива каждому объекту, то тут есть варианты:

    При создании объектов появится соответственно

    template < int ARRAY_LEN >class myClass < public: byte vals[ARRAY_LEN]; private: >; myClass obj1; // тут у obj1.vals будет 10 ячеек myClass obj2; // тут у obj2.vals будет 20 ячеек myClass obj3; // тут у obj3.vals будет 30 ячеек

    Также можно записать размер массива в переменную, чтобы использовать в дальнейшем коде:

    template < int ARRAY_LEN >class myClass < public: byte vals[ARRAY_LEN]; byte arrSize = ARRAY_LEN; private: >; myClass obj1; // obj1.vals имеет 30 ячеек // obj1.arrSize имеет значение 30

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

    Можно выделить “массив” динамически и хранить его как указатель. В качестве “константного” значения используется фишка С++ под названием список инициализации (двоеточие после myClass(int x) ):

    class myClass < public: int* arr; myClass(int x) : arr(new int[x]) < // конструктор >private: >; myClass obj(5); void setup() < Serial.begin(9600); obj.arr[0] = 1; obj.arr[1] = 2; obj.arr[2] = 3; obj.arr[3] = 4; obj.arr[4] = 5; for (byte i = 0; i < 5; i++) < Serial.println(obj.arr[i]); // выведет 1 2 3 4 5 с переносом >> void loop()

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

    Делаем константы

    Вы наверное часто видели в библиотеках передачу константы в функцию, далеко ходить не надо: digitalWrite(13, HIGH); , где HIGH – что это? Если вы откроете Arduino.h, то найдёте там HIGH, это – константа, дефайн:

    #define HIGH 0x1

    А в keywords.txt она указана как LITERAL1, что и даёт ей синий цвет. Давайте сделаем библиотеку, которая выводит текст в зависимости от указанной константы:

    #pragma once #include // константы #define KEK 0 #define LOL 1 #define AZAZA 2 #define HELLO 3 class testClass < public: void printer(byte value); private: >;

    testLib.cpp

    #include // подключаем заголовок обязательно void testClass::printer(byte value) < switch (value) < case 0: Serial.println("kek"); break; case 1: Serial.println("lol"); break; case 2: Serial.println("azaza"); break; case 3: Serial.println("hello"); break; >>

    testSketch.ino
    #include testClass testObject; void setup() < Serial.begin(9600); testObject.printer(KEK); // выведет kek testObject.printer(LOL); // выведет lol testObject.printer(AZAZA); // выведет azaza testObject.printer(HELLO); // выведет hello >void loop()

    Вот так можно передать вместо значения – слово, и работать с такой библиотекой будет удобнее. Заметьте, мы использовали константы (дефайн), это не очень правильно: если в другом подключенном ниже документе или в самом скетче наш дефайн совпадёт с названием другой переменной, функции или другого дефайна, то программа будет работать некорректно! Дефайн ведь распространяется на другие документы, включая главную программу (скетч). Что же делать? Можно называть свои константы настолько уникально, чтобы никто никогда с ними не пересекался, например добавлять префикс с названием библиотеки: MYLIB_CONSTANT. Ещё можно заменить дефайн перечислением, тогда ваша библиотека не будет влиять на другие и на главный документ, но вот другие библиотеки и внешние дефайны могут залезть и в вашу библиотеку… Смотрим пример, такой же как выше, но на enum:

    #pragma once #include // константы enum printModes < KEK, LOL, AZAZA, HELLO, >; class testClass < public: void printer(printModes value); private: >;

    testLib.cpp

    #include // подключаем заголовок обязательно void testClass::printer(printModes value) < switch (value) < case KEK: Serial.println("kek"); break; case LOL: Serial.println("lol"); break; case AZAZA: Serial.println("azaza"); break; case HELLO: Serial.println("hello"); break; >>

    testSketch.ino
    #include testClass testObject; void setup() < Serial.begin(9600); testObject.printer(KEK); // выведет kek testObject.printer(LOL); // выведет lol testObject.printer(AZAZA); // выведет azaza testObject.printer(HELLO); // выведет hello >void loop()

    Использование enum в голом виде тоже может приводить к проблемам: имена enum (как имя самого enum, так и его констант) не должны пересекаться в подключенных документах, иначе будет ошибка двойного объявления. О чём конкретно я говорю:

    Пересекающиеся enum

    // документ doc1.h enum engineControl < start, stop, restart, >; // документ doc2.h enum soundControl < play, pause, stop, replay, >; // основной документ #include "doc1.h" #include "doc2.h" // уже приведёт к ошибке "значение stop объявлено в другом месте"

    Чтобы разделить enum в разных файлах, т.е. изолировать их значения друг от друга, есть три способа:

    • Пространство имён namespace
    • enum class
    • Внесение enum в основной класс библиотеки

    Рассмотрим пример через namespace

    // документ doc1.h namespace engine < enum engineControl < start, stop, restart, >; >; // документ doc2.h namespace sound < enum soundControl < play, pause, stop, replay, >; >; // основной документ #include "doc1.h" #include "doc2.h" // используем соответствующие namespace'ы engine::engineControl control1 = engine::stop; sound::soundControl sound1 = sound::stop;

    enum class

    // документ doc1.h enum class engineControl < start, stop, restart, >; // документ doc2.h enum class soundControl < play, pause, stop, replay, >; // основной документ #include "doc1.h" #include "doc2.h" // используем пространство имён для ЗНАЧЕНИЙ enum engineControl control1 = engineControl::stop; soundControl sound1 = soundControl::stop;

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

    #pragma once #include // константы enum class printModes < KEK, LOL, AZAZA, HELLO, >; class testClass < public: void printer(printModes value); private: >;

    testLib.cpp

    #include "testLib.h" // подключаем заголовок обязательно void testClass::printer(printModes value) < switch (value) < case printModes::LOL: Serial.println("kek"); break; case printModes::KEK: Serial.println("lol"); break; case printModes::AZAZA: Serial.println("azaza"); break; case printModes::HELLO: Serial.println("hello"); break; >>

    testSketch.ino
    #include "testLib.h" testClass testObject; void setup() < Serial.begin(9600); testObject.printer(printModes::KEK); // выведет kek testObject.printer(printModes::LOL); // выведет lol testObject.printer(printModes::AZAZA); // выведет azaza testObject.printer(printModes::HELLO); // выведет hello >void loop()

    Ещё одним вариантом является объявление enum внутри класса, тогда обращаться к нему нужно будет через ИМЯ_КЛАССА::

    #include // нужно для ардуино-функций class testClass < public: enum printModes < KEK, LOL, AZAZA, HELLO, >; void printer(printModes value); private: >;

    testLib.cpp

    #include "testLib.h" // подключаем заголовок обязательно void testClass::printer(printModes value) < switch (value) < case LOL: Serial.println("kek"); break; case KEK: Serial.println("lol"); break; case AZAZA: Serial.println("azaza"); break; case HELLO: Serial.println("hello"); break; >>

    testSketch.ino
    #include "testLib.h" testClass testObject; void setup() < Serial.begin(9600); testObject.printer(testClass::KEK); // выведет kek testObject.printer(testClass::LOL); // выведет lol testObject.printer(testClass::AZAZA); // выведет azaza testObject.printer(testClass::HELLO); // выведет hello >void loop()

    Вмешательство в компиляцию

    Далее рассмотрим такую ситуацию: мы умеем пользоваться директивами препроцессора и хотим влиять на процесс компиляции библиотеки, не трогая ничего в файле библиотеки. Возможно ли это? Да, возможно. Важный момент: данный трюк работает только в заголовочном файле библиотеки, то есть от файла реализации .cpp скорее всего придётся отказаться. Если сделать define до подключения файла библиотеки, то этот дефайн будет “виден” из заголовочного файла библиотеки и его можно использовать для операторов условной компиляции. Важный момент: при создании библиотеки не рекомендуется писать исполнительный код в заголовочном файле вне класса, потому что это приведёт к ошибкам при подключении библиотеки в разных файлах. Для использования “магии дефайнов” нужно правильно оформить реализацию в заголовочном файле, смотрим пример:

    Вот так можно.h

    // lib.h class testClass < public: int func() private: >;

    А вот так нельзя.h
    // lib.h class testClass < public: int func(); private: >; int testClass::func()
    Ну и пример как работает дефайн, “влезающий” в библиотеку:

    #pragma once #include void printResult() < // если определена SEND_NUDES #ifdef SEND_NUDES Serial.begin(9600); Serial.println("nudes"); #endif >

    testSketch.ino
    // дефайним SEND_NUDES // ДО подключения библиотеки #define SEND_NUDES #include void setup() < // выведет "nudes" если задефайнен SEND_NUDES printResult(); >void loop()

    Зачем это нужно? Условная компиляция позволяет управлять компиляцией кода, то есть жёстко задавать, какие части кода будут компилироваться, а какие – нет. Более подробно об опасностях и тонкостях работы с define, в том числе и для создания библиотек, читайте в предыдущем уроке про директивы препроцессора.

    Полезные страницы

    • Набор GyverKIT – большой стартовый набор Arduino моей разработки, продаётся в России
    • Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress у проверенных продавцов
    • Подборка библиотек для Arduino, самых интересных и полезных, официальных и не очень
    • Полная документация по языку Ардуино, все встроенные функции и макросы, все доступные типы данных
    • Сборник полезных алгоритмов для написания скетчей: структура кода, таймеры, фильтры, парсинг данных
    • Видео уроки по программированию Arduino с канала “Заметки Ардуинщика” – одни из самых подробных в рунете
    • Поддержать автора за работу над уроками
    • Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *