Практическое применение булевой арифметики. Часть третья - Работаем с булевыми операциями в С++ и Qt
В прошлых частях мы рассмотрели основы булевой арифметики и двоичную систему счисления. Пришло время на практике применить эти знания.
Работать мы будем в QtCreator на языке программирования C++.
Создадим новый проект в Qtcreator
Qt Widgets Application
Назовем его
Bool1
Весь код будем добавлять в конструктор MainWindow::MainWindow
После строки:
ui->setupUi(this);
Добавим:
unsigned char a;
unsigned char b;
a = 0b00000000;
b = 0b00000001;
qDebug() << "a:" << QString("%1").arg(a, 8, 2, QChar('0')) << "dec:" << a;
qDebug() << "b:" << QString("%1").arg(b, 8, 2, QChar('0')) << "dec:" << b;
Запустим, результат:
a: "00000000" dec: 0
b: "00000001" dec: 1
Напишем функцию для упрощения отладки:
void printBoolVal(unsigned char val, QString name, bool isDebug = false) {
if (isDebug) {
qDebug() << name << "" << " 87654321";
}
qDebug() << name << "" << QString("%1").arg(val, 8, 2, QChar('0')) << "dec:" << val;
}
Изменим код:
unsigned char a;
unsigned char b;
a = 0b00000000;
b = 0b10000000;
printBoolVal(a, "a", true);
printBoolVal(b, "b");
Результат:
"a" 87654321
"a" "00000000" dec: 0
"b" "10000000" dec: 128
Параметр isDebug выводит номера битов, для большей наглядности.
Логическое сложение на практике
Допустим у нас есть 8 устройств, датчиков, программ, функций, не важно. В общем у нас есть 8 сигналов вида включено/выключено или 0/1 и нам нужно максимально эффективно, с наименьшими затратами отправлять эти данные на сервер мониторинга.
Данные собираются в массив:
bool data[8] = {0,0,0,0,0,0,0,0}
Нам пришел сигнал о изменении статуса с устройств 2 и 6:
data[2] = true;
data[6] = true;
Так же у нас есть переменная, в которую мы должны сложить все статусы наших устройств. В последующем эти данные будут отправлены по последовательному порту на сервер мониторинга:
unsigned char status = 0b00000000;
Давайте установим соответствующие биты:
status = status | 0b00000010;
status = status | 0b00100000;
printBoolVal(status, "status", true);
Запустим, результат:
"status" 87654321
"status" "00100010" dec: 34
Как видите очень просто.
Для наглядности я не буду далее использовать операторы
|=, &=
и им подобные!
Создаем класс для обработки статусов устройств
Давайте оптимизируем процесс создадим класс DeviceManager.
Заголовок:
#ifndef DEVICEMANAGER_H
#define DEVICEMANAGER_H
#include <QString>
const unsigned char dataMask[8] = {
0b00000001,
0b00000010,
0b00000100,
0b00001000,
0b00010000,
0b00100000,
0b01000000,
0b10000000
};
const int DEVICE_1 = 0;
const int DEVICE_2 = 1;
const int DEVICE_3 = 2;
const int DEVICE_4 = 3;
const int DEVICE_5 = 4;
const int DEVICE_6 = 5;
const int DEVICE_7 = 6;
const int DEVICE_8 = 7;
class DeviceManager
{
public:
DeviceManager();
void setStatusOn(int id);
unsigned char getStatus() const;
bool getDeviceStatus(int id) const;
private:
unsigned char status = 0b00000000;
unsigned char data[8] = {0,0,0,0,0,0,0,0};
};
#endif // DEVICEMANAGER_H
Реализация:
#include "devicemanager.h"
DeviceManager::DeviceManager()
{
}
void DeviceManager::setStatusOn(int id) {
this->data[id] = 1;
this->status = this->status | dataMask[id];
}
unsigned char DeviceManager::getStatus() const
{
return this->status;
}
bool DeviceManager::getDeviceStatus(int id) const
{
return this->data[id];
}
Удалим наш код из MainWindow::MainWindow и добавим новый:
DeviceManager dm = DeviceManager();
qDebug() << "Device 1 status" << dm.getDeviceStatus(DEVICE_1);
qDebug() << "Device 2 status" << dm.getDeviceStatus(DEVICE_2);
qDebug() << "Device 3 status" << dm.getDeviceStatus(DEVICE_3);
qDebug() << "Device 4 status" << dm.getDeviceStatus(DEVICE_4);
qDebug() << "Device 5 status" << dm.getDeviceStatus(DEVICE_5);
qDebug() << "Device 6 status" << dm.getDeviceStatus(DEVICE_6);
qDebug() << "Device 7 status" << dm.getDeviceStatus(DEVICE_7);
qDebug() << "Device 8 status" << dm.getDeviceStatus(DEVICE_8);
qDebug() << "----------------";
dm.setStatusOn(DEVICE_2);
dm.setStatusOn(DEVICE_6);
qDebug() << "Device 1 status" << dm.getDeviceStatus(DEVICE_1);
qDebug() << "Device 2 status" << dm.getDeviceStatus(DEVICE_2);
qDebug() << "Device 3 status" << dm.getDeviceStatus(DEVICE_3);
qDebug() << "Device 4 status" << dm.getDeviceStatus(DEVICE_4);
qDebug() << "Device 5 status" << dm.getDeviceStatus(DEVICE_5);
qDebug() << "Device 6 status" << dm.getDeviceStatus(DEVICE_6);
qDebug() << "Device 7 status" << dm.getDeviceStatus(DEVICE_7);
qDebug() << "Device 8 status" << dm.getDeviceStatus(DEVICE_8);
qDebug() << "----------------";
printBoolVal(dm.getStatus(), "status", true);
Запустим:
Device 1 status false
Device 2 status false
Device 3 status false
Device 4 status false
Device 5 status false
Device 6 status false
Device 7 status false
Device 8 status false
----------------
Device 1 status false
Device 2 status true
Device 3 status false
Device 4 status false
Device 5 status false
Device 6 status true
Device 7 status false
Device 8 status false
----------------
"status" 87654321
"status" "00100010" dec: 34
Добавляем метод отладки
Приведенный выше код довольно громоздок, давайте добавим метод:
debugStatus()
в класс:
DeviceManager
Реализация:
void DeviceManager::debugStatus()
{
for (int i=0;i<8;i++) {
qDebug() << "Device" << (i+1) << "status" << this->getDeviceStatus(i);
}
}
Изменим основной код:
DeviceManager dm = DeviceManager();
dm.debugStatus();
qDebug() << "----------------";
dm.setStatusOn(DEVICE_2);
dm.setStatusOn(DEVICE_6);
dm.debugStatus();
qDebug() << "----------------";
printBoolVal(dm.getStatus(), "status", true);
Результат:
Device 1 status false
Device 2 status false
Device 3 status false
Device 4 status false
Device 5 status false
Device 6 status false
Device 7 status false
----------------
Device 1 status false
Device 2 status true
Device 3 status false
Device 4 status false
Device 5 status false
Device 6 status true
Device 7 status false
----------------
"status" 87654321
"status" "00100010" dec: 34
Результат тот же, но код стал в разы компактней!
В массиве dataMask мы объявили битовую маску для всех устройств, каждому устройству от 0 до 8 соответствует своя битовая маска.
При вызове setStatusOn используя логическое ИЛИ мы устанавливаем соответствующий бит статуса в 1 и устанавливаем в 1 соответствующий элемент массива data, который хранит статусы всех устройств.
Практическое применение логического умножения (И) и логического (НЕТ)
Добавим в класс DeviceManager новый метод:
void DeviceManager::setStatusOff(int id)
{
this->data[id] = 0;
this->status = this->status & ~ dataMask[id];
}
Изменим код для запуска:
DeviceManager dm = DeviceManager();
qDebug() << "----------------";
dm.debugStatus();
dm.setStatusOn(DEVICE_2);
dm.setStatusOn(DEVICE_6);
dm.setStatusOn(DEVICE_8);
dm.setStatusOn(DEVICE_1);
qDebug() << "----------------";
dm.debugStatus();
qDebug() << "----------------";
printBoolVal(dm.getStatus(), "status", true);
dm.setStatusOff(DEVICE_8);
qDebug() << "----------------";
dm.debugStatus();
qDebug() << "----------------";
printBoolVal(dm.getStatus(), "status", true);
Запустим, результат:
----------------
Device 1 status false
Device 2 status false
Device 3 status false
Device 4 status false
Device 5 status false
Device 6 status false
Device 7 status false
Device 8 status false
----------------
Device 1 status true
Device 2 status true
Device 3 status false
Device 4 status false
Device 5 status false
Device 6 status true
Device 7 status false
Device 8 status true
----------------
"status" 87654321
"status" "10100011" dec: 163
----------------
Device 1 status true
Device 2 status true
Device 3 status false
Device 4 status false
Device 5 status false
Device 6 status true
Device 7 status false
Device 8 status false
----------------
"status" 87654321
"status" "00100011" dec: 35
Мы активировали устройства 1, 2, 6, 8, а потом отключили устройство 8.
Здесь мы применили сразу две логических операции:
this->status = this->status & ~ dataMask[id];
Давайте добавим код, чтобы разобраться, как это работает:
qDebug() << "----------------";
qDebug() << "----------------";
unsigned char status = 0b10010001;
unsigned char mask = 0b10000000;
printBoolVal(status, "status", true);
printBoolVal(mask, "mask ");
printBoolVal(~mask, "~ mask");
printBoolVal(status & ~ mask, "result");
qDebug() << "----------------";
qDebug() << "----------------";
Запустим, результат:
----------------
----------------
"status" 87654321
"status" "10010001" dec: 145
"mask " "10000000" dec: 128
"~ mask" "01111111" dec: 127
"result" "00010001" dec: 17
----------------
----------------
Сначала выполняется операция:
~mask
Которая инвертирует нашу маску в значение:
01111111
Как мы рассматривали в предыдущей части, если любой из операндов И равен нулю, то результат будет равен нулю. Таким образом, мы сбрасываем только восьмой бит!
Практическое применение исключающего ИЛИ (XOR)
Ранее мы вскользь упоминали Исключающее ИЛИ (XOR).
Если ИЛИ (OR) устанавливает биты, то XOR полная его противоположность.
Операция XOR обозначается знаком ^
Давайте рассмотрим операции XOR:
0 ^ 1 = 0
0 ^ 1 = 1
1 ^ 0 = 1
1 ^ 1 = 0
Как видно из таблицы, XOR противоположен OR. Результат будет равен 1 только тогда, когда один из элементов равен 0, а второй 1!
Рассмотрим его практическое применение:
Допустим у нас устройства 2, 5 и 8 связаны между собой.
Когда устройство 2 выключается, автоматически выключаются устройства 5 и 8.
Нам нужно включать/выключать устройство 2.
Добавим метод flipDev2()
void DeviceManager::flipDevice2()
{
unsigned char mask = 0b10010010;
if (this->data[DEVICE_2]) {
this->data[DEVICE_2] = 0;
this->data[DEVICE_5] = 0;
this->data[DEVICE_8] = 0;
this->status = this->status ^ mask;
} else {
this->data[DEVICE_2] = 1;
this->data[DEVICE_5] = 1;
this->data[DEVICE_8] = 1;
this->status = this->status ^ mask;
}
}
Добавим код:
dm = DeviceManager();
dm.setStatusOn(DEVICE_2);
dm.setStatusOn(DEVICE_5);
dm.setStatusOn(DEVICE_8);
printBoolVal(dm.getStatus(), "status", true);
qDebug() << "----------------";
dm.debugStatus();
dm.flipDevice2();
qDebug() << "----------------";
printBoolVal(dm.getStatus(), "status", true);
qDebug() << "----------------";
dm.debugStatus();
dm.flipDevice2();
qDebug() << "----------------";
dm.debugStatus();
qDebug() << "----------------";
printBoolVal(dm.getStatus(), "status", true);
Запустим, результат:
"status" 87654321
"status" "10010010" dec: 146
----------------
Device 1 status false
Device 2 status true
Device 3 status false
Device 4 status false
Device 5 status true
Device 6 status false
Device 7 status false
Device 8 status true
----------------
"status" 87654321
"status" "00000000" dec: 0
----------------
Device 1 status false
Device 2 status false
Device 3 status false
Device 4 status false
Device 5 status false
Device 6 status false
Device 7 status false
Device 8 status false
----------------
Device 1 status false
Device 2 status true
Device 3 status false
Device 4 status false
Device 5 status true
Device 6 status false
Device 7 status false
Device 8 status true
----------------
"status" 87654321
"status" "10010010" dec: 146
XOR вместо включения битов, инвертирует их.
Проверка установленного бита или еще одно практическое использование И (AND)
Мы рассмотрели установку и сброс битов и теперь можем управлять статусом устройств.
Но помимо этого, нам может понадобится прочитать статус всех устройств из байта. Для этого идеально подойдет операция И.
Добавим метод readStatus:
void DeviceManager::readStatus(unsigned char status)
{
for (int i=0; i<8; i++) {
if (status & dataMask[i]) {
this->data[i] = 1;
}
}
}
Добавим:
qDebug() << "----------------";
dm = DeviceManager();
status = 0b10010101;
printBoolVal(status, "status", true);
qDebug() << "Device 1 status" << dm.getDeviceStatus(DEVICE_1);
qDebug() << "Device 2 status" << dm.getDeviceStatus(DEVICE_2);
qDebug() << "Device 3 status" << dm.getDeviceStatus(DEVICE_3);
qDebug() << "Device 4 status" << dm.getDeviceStatus(DEVICE_4);
qDebug() << "Device 5 status" << dm.getDeviceStatus(DEVICE_5);
qDebug() << "Device 6 status" << dm.getDeviceStatus(DEVICE_6);
qDebug() << "Device 7 status" << dm.getDeviceStatus(DEVICE_7);
qDebug() << "Device 8 status" << dm.getDeviceStatus(DEVICE_8);
qDebug() << "----------------";
printBoolVal(status, "status", true);
dm.ReadStatus(status);
qDebug() << "Device 1 status" << dm.getDeviceStatus(DEVICE_1);
qDebug() << "Device 2 status" << dm.getDeviceStatus(DEVICE_2);
qDebug() << "Device 3 status" << dm.getDeviceStatus(DEVICE_3);
qDebug() << "Device 4 status" << dm.getDeviceStatus(DEVICE_4);
qDebug() << "Device 5 status" << dm.getDeviceStatus(DEVICE_5);
qDebug() << "Device 6 status" << dm.getDeviceStatus(DEVICE_6);
qDebug() << "Device 7 status" << dm.getDeviceStatus(DEVICE_7);
qDebug() << "Device 8 status" << dm.getDeviceStatus(DEVICE_8);
Запустим:
"status" 87654321
"status" "10010101" dec: 149
Device 1 status false
Device 2 status false
Device 3 status false
Device 4 status false
Device 5 status false
Device 6 status false
Device 7 status false
Device 8 status false
----------------
"status" 87654321
"status" "10010101" dec: 149
Device 1 status true
Device 2 status false
Device 3 status true
Device 4 status false
Device 5 status true
Device 6 status false
Device 7 status false
Device 8 status true
Мы использовали & для проверки установлен ли выбранный бит.
Когда бит установлен мы получаем значение отличное от нуля. Таким образом мы можем проверить установлен или нет любой из битов.
Упаковка нескольких значений в байт
Давайте вспомним этот рисунок:
В одном байте мы можем хранить четыре значения от 0 до 3 или два от 0 до 15.
Допустим у нас есть четыре устройства, которые могут принимать более двух состояний, например, включено/выключено/предупреждение/ошибка.
Вместо того, чтобы отправлять информацию отдельными байтами, мы можем упаковать эти значения в один байт.
Объявим константы для статуса устройств:
const unsigned char DEVICE_ON = 0b00000000;
const unsigned char DEVICE_OFF = 0b00000001;
const unsigned char DEVICE_WRN = 0b00000010;
const unsigned char DEVICE_ERR = 0b00000011;
Добавим метод packStatus():
unsigned char DeviceManager::packStatus(unsigned char st1, unsigned char st2, unsigned char st3, unsigned char st4)
{
unsigned char d1_mask = 0;
unsigned char d2_mask = 0;
unsigned char d3_mask = 0;
unsigned char d4_mask = 0;
unsigned char status = 0;
d1_mask = st1;
d2_mask = st2 << 2;
d3_mask = st3 << 4;
d4_mask = st4 << 6;
status = status | d1_mask | d2_mask | d3_mask | d4_mask;
return status;
}
Добавим код:
status = 0;
qDebug() << "----------------";
printBoolVal(status, "status", true);
dm = DeviceManager();
status = dm.packStatus(DEVICE_WRN, DEVICE_WRN, DEVICE_WRN, DEVICE_WRN);
qDebug() << "----------------";
printBoolVal(status, "status", true);
Запустим, результат:
----------------
"status" 87654321
"status" "00000000" dec: 0
----------------
"status" 87654321
"status" "10101010" dec: 170
Мы заполнили байт статусом устройств, в данном случае статус представлен двумя битами 10.
Здесь всё просто, мы используем операцию операцию побитового сдвига влево, для того, чтобы заполнить байт нужными битами.
Чтобы было нагляднее я написал следующий код:
unsigned char dwrn = DEVICE_WRN ;
unsigned char d1_mask = 0;
unsigned char d2_mask = 0;
unsigned char d3_mask = 0;
unsigned char d4_mask = 0;
status = 0;
d1_mask = dwrn;
qDebug() << "----------------";
printBoolVal(d1_mask, "d1_mask", true);
d2_mask = dwrn << 2;
qDebug() << "----------------";
printBoolVal(d2_mask, "d2_mask", false);
d3_mask = dwrn << 4;
qDebug() << "----------------";
printBoolVal(d3_mask, "d3_mask", false);
d4_mask = dwrn << 6;
qDebug() << "----------------";
printBoolVal(d4_mask, "d4_mask", false);
status = status | d1_mask | d2_mask | d3_mask | d4_mask;
qDebug() << "----------------";
printBoolVal(status, "status", true);
Запустим:
"d1_mask" 87654321
"d1_mask" "00000010" dec: 2
----------------
"d2_mask" "00001000" dec: 8
----------------
"d3_mask" "00100000" dec: 32
----------------
"d4_mask" "10000000" dec: 128
----------------
"status" 87654321
"status" "10101010" dec: 170
Каждый раз, для создания битовой маски, мы сначала присваиваем значение с битами 10 переменной, и тут же сдвигаем его на указанное количество битов с помощью операции <<.
При этом статус устройства с номером 1 занимает первые два бита, соответственно статус устройства с номером 4 занимает последние два бита.
Распаковка значений из байта
Давайте рассмотрим обратное действие – распаковку значений из байта.
Создадим метод:
unsigned char* DeviceManager::unpackStatus(unsigned char status)
{
static unsigned char tmp[4];
tmp[0] = status & 0b00000011;
tmp[1] = status & 0b00001100;
tmp[1] = tmp[1] >> 2;
tmp[2] = status & 0b00110000;
tmp[2] = tmp[2] >> 4;
tmp[3] = status & 0b11000000;
tmp[3] = tmp[3] >> 6;
return tmp;
}
Здесь мы используем битовую маску, логическое и операцию побитового сдвига вправо.
Для первого значения мы просто применяем маску, обнуляя все биты слева.
Для остальных мы сначала используем маску, а потом используем побитовый сдвиг вправо, на соответствующее количество битов, чтобы биты с нашим статусом оказались в двух первых битах.
Добавим код:
unsigned char status = 0b11011011;
unsigned char* stats;
stats = dm.unpackStatus(status);
qDebug() << "----------------";
printBoolVal(stats[0], "st1", true);
printBoolVal(stats[1], "st2", false);
printBoolVal(stats[2], "st3", false);
printBoolVal(stats[3], "st4", false);
Запустим, результат:
----------------
"st1" 87654321
"st1" "00000011" dec: 3
"st2" "00000010" dec: 2
"st3" "00000001" dec: 1
"st4" "00000011" dec: 3
Мы так же можем использовать результат функции для упаковки значений:
status = dm.packStatus(DEVICE_WRN, DEVICE_OFF, DEVICE_ON, DEVICE_ERR);
stats = dm.unpackStatus(status);
qDebug() << "----------------";
printBoolVal(stats[0], "st1", true);
printBoolVal(stats[1], "st2", false);
printBoolVal(stats[2], "st3", false);
printBoolVal(stats[3], "st4", false);
Запустим, результат:
----------------
"st1" 87654321
"st1" "00000010" dec: 2
"st2" "00000000" dec: 0
"st3" "00000001" dec: 1
"st4" "00000011" dec: 3
Заключение
Сегодня мы рассмотрели практическое применение булевой арифметики.
Создали класс для установки и управления статусов устройств.
Рассмотрели операции И, ИЛИ, НЕ, XOR для управления статусами устройств.
Добавить комментарий