Union c что это
Перейти к содержимому

Union c что это

  • автор:

Типы struct, union и enum в Modern C++

Язык C++ сильно изменился за последние 10 лет. Изменились даже базовые типы: struct, union и enum. Сегодня мы кратко пройдёмся по всем изменениям от C++11 до C++17, заглянем в C++20 и в конце составим список правил хорошего стиля.

Зачем нужен тип struct

Тип struct — фундаментальный. Согласно C++ Code Guidelines, struct лучше использовать для хранения значений, не связанных инвариантом. Яркие примеры — RGBA-цвет, вектора из 2, 3, 4 элементов или информация о книге (название, количество страниц, автор, год издания и т.п.).

struct BookStats < std::string title; std::vectorauthors; std::vector tags; unsigned pageCount = 0; unsigned publishingYear = 0; >;

Он похож на class, но есть два мелких различия:

  • по умолчанию в struct действует видимость public, а в class — private
  • по умолчанию struct наследует члены базовых структур/классов как публичные члены, а class — как приватные члены
// поле data публичное struct Base < std::string data; >; // Base унаследован так, как будто бы написано `: public Base` struct Derived : Base < >;

Согласно C++ Core Guidelines, struct хорошо применять для сокращения числа параметров функции. Этот приём рефакторинга известен как «parameter object».

Кроме того, структуры могут сделать код более лаконичным. Например, в 2D и 3D графике удобнее считать в 2-х и 3-х компонентных векторах, чем в числах. Ниже показан код, использующий библиотеку GLM (OpenGL Mathematics)

// Преобразует полярные координаты в декартовы // См. https://en.wikipedia.org/wiki/Polar_coordinate_system glm::vec2 euclidean(float radius, float angle) < return < radius * cos(angle), radius * sin(angle) >; > // Функция делит круг на треугольники, // возвращает массив с вершинами треугольников. std::vector TesselateCircle(float radius, const glm::vec2& center, IColorGenerator& colorGen) < assert(radius >0); // Круг аппроксимируется с помощью треугольников. // Внешняя сторона каждого треугольника имеет длину 2. constexpr float step = 2; // Число треугольников равно длине окружности, делённой на шаг по окружности. const auto pointCount = static_cast(radius * 2 * M_PI / step); // Вычисляем точки-разделители на окружности. std::vector points(pointCount); for (unsigned pi = 0; pi < pointCount; ++pi) < const auto angleRadians = static_cast(2.f * M_PI * pi / pointCount); points[pi] = center + euclidean(radius, angleRadians); > return TesselateConvexByCenter(center, points, colorGen); >

Эволюция struct

В C++11 появилась инициализация полей при объявлении.

struct BookStats < std::string title; std::vectorauthors; std::vector tags; unsigned pageCount = 0; unsigned publishingYear = 0; >;

Ранее для таких целей приходилось писать свой конструктор:

// ! устаревший стиль ! struct BookStats < BookStats() : pageCount(0), publishingYear(0) <>std::string title; std::vector authors; std::vector tags; unsigned pageCount; unsigned publishingYear; >;

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

// C++11, C++14: будет ошибка компиляции из-за инициализаторов pageCount и publishingYear // C++17: компиляция проходит const auto book = BookStats< u8"Незнайка на Луне", < u8"Николай Носов" >, < u8"детская", u8"фантастика" >, 576, 1965 >;

В C++11 и C++14 это решалось вручную написанием конструктора с boilerplate кодом. В C++17 ничего дописывать не надо — стандарт явно разрешает агрегатную инициализацию для структур с инициализаторами полей.

В примере написаны конструкторы, необходимые только в C++11 и C++14:

struct BookStats < // ! устаревший стиль! BookStats() = default; // ! устаревший стиль! BookStats( std::string title, std::vectorauthors, std::vector tags, unsigned pageCount, unsigned publishingYear) : title(std::move(title)) , authors(std::move(authors)) , tags(std::move(authors)) // ;) , pageCount(pageCount) , publishingYear(publishingYear) < >std::string title; std::vector authors; std::vector tags; unsigned pageCount = 0; unsigned publishingYear = 0; >;

В C++20 агрегатная инициализация обещает стать ещё лучше! Чтобы понять проблему, взгляните на пример ниже и назовите каждое из пяти инициализируемых полей. Не перепутан ли порядок инициализации? Что если кто-то в ходе рефакторинга поменяет местами поля в объявлении структуры?

const auto book = BookStats< u8"Незнайка на Луне", < u8"Николай Носов" >, < u8"детская", u8"фантастика" >, 1965, 576 >;

В C11 появилась удобная возможность указать имена полей при инициализации структуры. Эту возможность обещают включить в C++20 под названием «назначенный инициализатор» («designated initializer»). Подробнее об этом в статье Дорога к С++20.

// Должно скомпилироваться в C++20 const auto book = BookStats< .title = u8"Незнайка на Луне", .authors = < u8"Николай Носов" >, .tags = < u8"детская", u8"фантастика" >, .publishingYear = 1965, .pageCount = 576 >;

В C++17 появился structured binding, также известный как «декомпозиция при
объявлении». Этот механизм работает со структурами, с std::pair и std::tuple и дополняет агрегатную инициализацию.

// композиция структуры const auto book = BookStats< u8"Незнайка на Луне", < u8"Николай Носов" >, < u8"детская", u8"фантастика" >, 576, 1965 >; // декомпозиция структуры const auto [title, authors, tags, pagesCount, publishingYear] = book;

В сочетании с классами STL эта фишка может сделать код элегантнее:

#include #include #include #include int main() < std::mapmap = < < "hello", 1 >, < "world", 2 >, < "it's", 3 >, < "me", 4 >, >; // пример №1 - разложение пары [iterator, bool] auto [helloIt, helloInserted] = map.insert_or_assign("hello", 5); auto [goodbyeIt, goodbyeInserted] = map.insert_or_assign("goodbye", 6); assert(helloInserted == false); assert(goodbyeInserted == true); // пример №2 - разложение пары [key, value] for (auto&& [ key, value ] : map) std::cout Зачем нужен тип union 

Вообще-то в C++17 он не нужен в повседневном коде. C++ Core Guidelines предлагают строить код по принципу статической типобезопасности, что позволяет компилятору выдать ошибку при откровенно некорректной обработке данных. Используйте std::variant как безопасную замену union.


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


// ! этот код ужасно устрарел ! // Event имет три поля: type, mouse, keyboard // Поля mouse и keyboard лежат в одной области памяти struct Event < enum EventType < MOUSE_PRESS, MOUSE_RELEASE, KEYBOARD_PRESS, KEYBOARD_RELEASE, >; struct MouseEvent < unsigned x; unsigned y; >; struct KeyboardEvent < unsigned scancode; unsigned virtualKey; >; EventType type; union < MouseEvent mouse; KeyboardEvent keyboard; >; >;

Эволюция union

В C++11 вы можете складывать в union типы данных, имеющие собственные конструкторы. Вы можете объявить свой констуктор union. Однако, наличие конструктора ещё не означает корректную инициализацию: в примере ниже поле типа std::string забито нулями и вполне может быть невалидным сразу после конструирования union (на деле это зависит от реализации STL).

// ! этот код ужасно устрарел ! union U < unsigned a = 0; std::string b; U() < std::memset(this, 0, sizeof(U)); >>; // нельзя так писать - поле b может не являться корректной пустой строкой U u; u.b = "my value";

В C++17 код мог бы выглядеть иначе, используя variant. Внутри variant использует небезопасные конструкции, которые мало чем отличаются от union, но этот опасный код скрыт внутри сверхнадёжной, хорошо отлаженной и протестированной STL.

#include struct MouseEvent < unsigned x = 0; unsigned y = 0; >; struct KeyboardEvent < unsigned scancode = 0; unsigned virtualKey = 0; >; using Event = std::variant< MouseEvent, KeyboardEvent>;

Зачем нужен тип enum

Тип enum хорошо использовать везде, где есть состояния. Увы, многие программисты не видят состояний в логике программы и не догадываются применить enum.

Ниже пример кода, где вместо enum используют логически связанные булевы поля. Как думаете, будет ли класс работать корректно, если m_threadShutdown окажется равным true, а m_threadInitialized — false?

// ! плохой стиль ! class ThreadWorker < public: // . private: bool m_threadInitialized = false; bool m_threadShutdown = false; >;

Мало того что здесь не используется atomic, который скорее всего нужен в классе с названием Thread* , но и булевы поля можно заменить на enum.

class ThreadWorker < public: // . private: enum class State < NotStarted, Working, Shutdown >; // С макросом ATOMIC_VAR_INIT вы корректно проинициализируете atomic на всех платформах. // Менять состояние надо через compare_and_exchange_strong! std::atomic = ATOMIC_VAR_INIT(State::NotStarted); >;

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

// ! плохой стиль ! void FillSlide(unsigned slideNo) < switch (slideNo) < case 1: setTitle(". "); setPictureAt(. ); setTextAt(. ); break; case 2: setTitle(". "); setPictureAt(. ); setTextAt(. ); break; // . >>

Даже если хардкод слайдов оправдан, ничто не может оправдать магические числа. Их легко заменить на enum, и это по крайней мере повысит читаемость.

enum SlideId < Slide1 = 1, Slide2, Slide3, Slide4 >;

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

// ! этот код - сомнительный ! enum TextFormatFlags < TFO_ALIGN_CENTER = 1 ; unsigned flags = TFO_ALIGN_CENTER; if (useBold) < flags = flags | TFO_BOLD; >if (alignLeft) < flag = flags & ~TFO_ALIGN_CENTER; >const bool isBoldCentered = (flags & TFO_BOLD) && (flags & TFO_ALIGN_CENTER);

Возможно, вам лучше использовать std::bitset :

enum TextFormatBit < TextFormatAlignCenter = 0, TextFormatItalic, TextFormatBold, // Значение последней константы равно числу элементов, // поскольку первый элемент равен 0, и без явно // указанного значения константа на 1 больше предыдущей. TextFormatCount >; std::bitset flags; flags.set(TextFormatAlignCenter, true); if (useBold) < flags.set(TextFormatBold, true); >if (alignLeft) < flags.set(TextFormatAlignCenter, false); >const bool isBoldCentered = flags.test(TextFormatBold) || flags.test(TextFormatAlignCenter);

Иногда программисты записывают константы в виде макросов. Такие макросы легко заменить на enum или constexpr.

// ! плохой стиль - даже в C99 этого уже не требуется ! #define RED 0xFF0000 #define GREEN 0x00FF00 #define BLUE 0x0000FF #define CYAN 0x00FFFF // стиль, совместимый с C99, но имена констант слишком короткие enum ColorId : unsigned < RED = 0xFF0000, GREEN = 0x00FF00, BLUE = 0x0000FF, CYAN = 0x00FFFF, >; // стиль Modern C++ enum class WebColorRGB < Red = 0xFF0000, Green = 0x00FF00, Blue = 0x0000FF, Cyan = 0x00FFFF, >;

Эволюция enum

В С++11 появился scoped enum, он же enum class или enum struct . Такая модификация enum решает две проблемы:

  • область видимости констант enum class — это сам enum class, т.е. снаружи вместо Enum e = EnumValue1 вам придётся писать Enum e = Enum::Value1 , что гораздо нагляднее
  • enum конвертируется в целое число без ограничений, а в enum class для этого потребуется static cast: const auto value = static_cast(Enum::Value1)

Кроме того, для enum и scoped enum появилась возможность явно выбрать тип, используемый для представления перечисления в сгенерированном компилятором коде:

enum class Flags : unsigned < // . >;

В некоторых новых языках, таких как Swift или Rust, тип enum по умолчанию является строгим в преобразованиях типов, а константы вложены в область видимости типа enum. Кроме того, поля enum могут нести дополнительные данные, как в примере ниже

// enum в языке Swift enum Barcode < // вместе с константой upc хранятся 4 поля типа Int case upc(Int, Int, Int, Int) // вместе с константой qrCode хранится поле типа String case qrCode(String) >

Такой enum эквивалентен типу std::variant , вошедшему в C++ в стандарте C++ 2017. Таким образом, std::variant заменяет enum в поле структуры и класса, если этот enum по сути обозначает состояние. Вы получаете гарантированное соблюдение инварианта хранимых данных без дополнительных усилий и проверок. Пример:

struct AnonymousAccount < >; struct UserAccount < std::string nickname; std::string email; std::string password; >; struct OAuthAccount < std::string nickname; std::string openId; >; using Account = std::variant;

Правила хорошего стиля

Подведём итоги в виде списка правил:

  • C.1: организуйте логически связанные данные в структуры или классы
  • C.2: используйте class если данные связаны инвариантом; используйте struct если данные могут изменяться независимо
    • C.8: используйте class вместо struct, если хотя бы одно поле не публичное
    • вместо out-параметров возвращайте из функции структуру или кортеж
    • не инициализируйте поля нулями в конструкторах, полагайтесь на инициализаторы полей
    • предпочитайте std::variant , если в разных состояниях класс способен хранить разные поля данных
    • используйте старый enum если вам крайне важна неявная конвертация enum в целое число
    • используйте enum class или enum вместо магических чисел
    • используйте enum class , enum или constexpr вместо макросов-констант

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

    C++. Объединения. Ключевое слово union. Примеры объявления и использования объединений

    Объединения. Ключевое слово union . Примеры объявления и использования объединений

    Поиск на других ресурсах:

    1. Что такое объединение в языке C++? Для чего используются объединения?

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

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

    Точно также как и структуры , объединения требуют объявления типа (шаблона) и объявления переменной этого типа.

    2. Как выглядит общая форма объявления типа (шаблона) объединения? Ключевое слово union

    Объявление объединения (типа объединения или шаблона объединения) начинается с ключевого слова union .

    union имя_типа_объединения < тип переменная1; тип переменная2; . тип переменнаяN; >;
    • имя_типа_объединения – непосредственно имя новосозданного типа;
    • переменная1 , переменная2 , переменнаяN – переменные, которые есть полями объединения. Эти переменные могут быть разных типов;
    • тип – тип переменной, который есть полем объединения.

    Тип переменной может быть:

    3. Что такое длина объединения? Как вычисляется длина объединения?

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

    Длина объединения вычисляется как максимум из всех длин (размеров в байтах) отдельных полей шаблона. Следует напомнить, что одно поле – это объявление одной переменной в объединении (см. п. 2).

    4. Как объявить тип (шаблон) объединения и переменную этого типа? Пример

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

    // объявление типа "объединение Floats" union Floats < float f; // рассматривается 4 байта double d; // рассматривается 8 байт >;

    Тип объединения Floats содержит 2 переменные с именами f и d . Переменная f есть типа float, переменная d есть типа double . Для переменной f типа float рассматривается (принимается во внимание) 4 байта. Для переменной d типа double принимается во внимание 8 байт, так как компилятор выделяет для этого типа именно 8 байт.

    Чтобы использовать объединение Floats в другом программном коде (методе, обработчике события и т.п.) нужно объявить переменную типа Floats как показано ниже

    Floats Fl; int d; Fl.f = 20.5; // Fl.d не определено Fl.d = -100.35; // теперь Fl.f не определено d = sizeof(Fl); // d = 8

    Поскольку размещение переменных в памяти условно начинается с одного адреса, то для переменной Fl типа Floats выделяется 8 байт памяти. Это связано с тем, что переменная типа double требует больше памяти для своего представления чем переменная типа float .

    На рисунке 1 отображено размещение (интерпретация) переменных f , d из объединения Floats .

    C++ объединение поля представление

    Рис. 1. Представление переменных f , d в объединении Floats

    5. Как осуществляется доступ к полям объединения?

    Доступ к полям объединения осуществляется так же, как и для структуры:

    • с помощью символа ‘ . ‘ ;
    • с помощью последовательности символов ‘->’ в случае, когда объявлена переменная-указатель на объединение.
    6. Пример объявления и использования указателя ( * ) на объединение

    Работа объединений с неуправляемыми ( * ) указателями точно такая же, как и работа структур с неуправляемыми указателями .

    В нижеследующем примере объявляется неуправляемый указатель на объединение типа Ints

    // указатель на объединение Ints *p; // неуправляемый указатель // выделить память для объединения p = new Ints; // доступ к полям с помощью указателя pI->a = 200; pI->b = 3400;
    7. Как объявить вложенные объединения (структуры, классы) в шаблоне объединения? Пример

    Шаблон объединения может включать поля, что есть структурами, объединениями и классами.

    В примере ниже объявляется шаблон объединения с именем Types , содержащий два вложенных объединения Floats и Ints , структуру ArrayOfChars и класс MyPoint .

    Объявление структур и объединений имеет следующий вид

    // объединение целочисленных типов union Ints < unsigned short int a; unsigned int b; unsigned long int c; >; // структура, содержащая 2 строки struct ArrayOfChars < char A[10]; char B[8]; >; // объявление типа "объединение Floats" union Floats < float f; // рассматривается 4 байта double d; // рассматривается 8 байт >;

    Объявление шаблона класса имеет следующий вид:

    // класс объявляется в отдельном модуле, например MyPoint.h #pragma once class MyPoint < public: int x; int y; // методы класса int GetX(void) < return x; > int GetY(void) < return y; > void SetXY(int nx, int ny) < x = nx; y = ny; >>;

    Объявление типа объединение Types с вложенными сложными типами Ints , Floats , ArrayOfChars , MyPoint .

    // подключить модуль с объявленным классом MyPoint #include "MyPoint.h" . // объявление типа "объединение Types" union Types < Floats Fl; // объединение Ints I; // объединение ArrayOfChars A; // структура MyPoint MP; // класс >;

    Использование объединения Types в некотором программном коде:

    // объявить переменную типа "объединение Types" Types T; // изменить значения полей переменной T T.Fl.f = (float)20.35; // объединение Floats T.I.b = 230; // объединение Ints T.A.A[2] = 'A'; // структура ArrayOfChars T.MP.SetXY(3,8); // класс MyPoint int d; d = T.MP.GetX(); // d = 3
    8. Как описать массив объединений? Пример

    // Пример объявления и использования массива объединений Floats F[5]; // объявляется массив из 5 объединений типа Floats // заполнение значений полей for (int i=0; i

    9. Какие особенности применения операции sizeof () для объединений и структур?

    В программах на C++ для определения размера переменной типа «структура» или «объединение» обязательно нужно использовать операцию sizeof . Определение размера «вручную» есть ошибочным поскольку:

    • размеры некоторых встроенных типов (например тип int ) могут быть разными для разных компьютеров. Например, на одних платформах для типа int будет выделено 2 байта, на других 4 байта;
    • компилятор делает так называемое «выравнивание памяти» на границе слова (2 байта) или абзаца (16 байт). Например, если компилятор делает выравнивание на границе абзаца, то структура (объединение) типа ArraysOfChars :
    // структура, содержащая 2 строки struct ArrayOfChars < char A[10]; char B[8]; >;

    может занимать в памяти 24 байта. Так как для массива A выделяется 16 байт а не 10 байт. Компилятор дополнительно выделяет 6 байт чтобы реализовать выравнивание на границе абзаца.

    Таким образом, использование операции sizeof() для определения типа структуры или объединения гарантирует переносность программного кода.

    Связанные темы

    • Структуры. Составные типы данных. Шаблон структуры. Структурная переменная. Структуры в среде CLR . Объявление и инициализация структурной переменной
    • Массивы. Определение массива. Одномерные массивы. Инициализация массива

    Объединение (union) в языке C для упаковки и распаковки данных

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

    Использование объединений для упаковки/распаковки данных

    Члены объединения хранятся в общей области памяти. Это ключевая особенность, которая позволяет нам находить интересные применения для объединений.

    Рассмотрим объединение, приведенное ниже:

    union < uint16_t word; struct < uint8_t byte1; uint8_t byte2; >; > u1;

    Внутри этого объединения содержится два члена. Первый член, “ word ”, является двухбайтовой переменной. Второй член – это структура из двух однобайтовых переменных. Два байта, выделенные для объединения распределяются между двумя его членами.

    Выделенное пространство памяти может быть таким, как показано ниже на рисунке 1.

    Рисунок 1 – Выделенное пространство памяти для объединения, приведенного в коде выше

    В то время как переменная “ word ” относится ко всему выделенному пространству памяти, переменные “ byte1 ” и “ byte2 ” относятся к однобайтовым областям, которые составляют переменную “ word ”. Как мы можем использовать эту особенность? Предположим, что у вас есть две однобайтовые переменные, “ x ” и “ y ”, которые должны быть объединены для получения одной двухбайтовой переменной.

    В этом случае вы можете использовать приведенное выше объединение и присвоить значения “ x ” и “ y ” членам структуры следующим образом:

    u1.byte1 = y; u1.byte2 = x;

    Теперь мы можем прочитать у объединения член “ word ”, чтобы получить двухбайтовую переменную, состоящую из переменных “ x ” и “ y ” (рисунок 2).

    Рисунок 2 – Упаковка двух однобайтовых переменных с помощью объединения

    В приведенном выше примере показано использование объединений для упаковки двух однобайтовых переменных в одну двухбайтовую переменную. Мы также можем сделать и обратное: записать двухбайтовое значение в “ word ” и распаковать его в две однобайтовые переменные, прочитав переменные “ x ” и “ y ”. Запись значения в один член объединения и чтение другого члена иногда называется «каламбуром данных» («data punning»)

    Порядок байтов процессора

    При использовании объединений для упаковки/распаковки данных мы должны быть осторожны с порядком байтов (endianness) процессора. Как обсуждалось в статье о порядке байтов, этот термин определяет порядок, в котором байты объекта хранятся в памяти. Процессор может быть с обратным порядком (от младшего к старшему, little endian) или с прямым порядком (от старшего к младшему, big endian). В системах с прямым порядком байтов (big endian) данные хранятся таким образом, что байт, содержащий старший значащий байт, имеет самый младший адрес памяти. В системах с обратным порядком байтов (little endian) байт, содержащий младший значащий байт, сохраняется первым.

    Пример, изображенный на рисунке 3, иллюстрирует хранение последовательности 0x01020304 при обратном и при прямом порядках байтов.

    Рисунок 3 – Хранение последовательности 0x01020304 при обратном и при прямом порядках байтов

    Давайте поэкспериментируем с объединением из предыдущего раздела.

    #include #include int main() < union < struct< uint8_t byte1; uint8_t byte2; >; uint16_t word; > u1; u1.byte1 = 0x21; u1.byte2 = 0x43; printf("Word is: %#X", u1.word); return 0; >

    Запустив данный код, я получаю следующий вывод

    Word is: 0X4321

    Он показывает, что первый байт общего пространства памяти (“ u1.byte1 ”) используется для хранения наименьшего значащего байта ( 0x21 ) переменной “ word ”. Другими словами, процессор, который я использую для выполнения кода, использует обратный порядок байтов (little endian).

    Как видите, это конкретное применение объединений может демонстрировать поведение, зависящее от реализации. Тем не менее, это не должно быть серьезной проблемой, потому что при таком низкоуровневом кодировании мы обычно знаем порядок байтов процессора. В случае если мы не знаем подробностей, мы можем использовать приведенный выше код, чтобы узнать, как данные организованы в памяти.

    Альтернативное решение

    Вместо использования объединений для выполнения упаковки или распаковки данных мы также можем использовать побитовые операторы. Например, мы можем использовать следующий код для совмещения двух однобайтовых переменных, “ byte3 ” и “ byte4 ”, и создания одной двухбайтовой переменной (“ word2 ”):

    word2 = (((uint16_t) byte3) 

    Давайте сравним вывод этих двух решений в случаях с прямым и обратным порядками байтов. Рассмотрим код, приведенный ниже:

    #include #include int main() < union < struct < uint8_t byte1; uint8_t byte2; >; uint16_t word1; > u1; u1.byte1 = 0x21; u1.byte2 = 0x43; printf("Word1 is: %#X\n", u1.word1); uint8_t byte3, byte4; uint16_t word2; byte3 = 0x21; byte4 = 0x43; word2 = (((uint16_t) byte3)

    Если мы скомпилируем этот код для процессора с прямым порядком байтов (big endian), такого как TMS470MF03107, результат будет следующим:

    Word1 is: 0X2143 Word2 is: 0X2143

    Однако если мы скомпилируем этот код для процессора с обратным порядком байтов (little endian), такого как STM32F407IE, результат будет следующим:

    Word1 is: 0X4321 Word2 is: 0X2143

    В то время как основанный на объединении код демонстрирует аппаратно-зависимое поведение, способ, основанный на операции сдвига, приводит к одному и тому же результату независимо от порядка байтов процессора. Это связано с тем, что при последнем подходе мы присваиваем значение переменной по имени (“ word2 ”), и компилятор заботится об организации памяти, используемой устройством. А с помощью метода на основе объединения мы меняем значение байтов, которые составляют переменную “ word1 ”.

    Хотя основанный на объединении метод демонстрирует аппаратно-зависимое поведение, его преимущество заключается в том, что он более читабелен и удобен в поддержке. Вот почему многие программисты предпочитают использовать объединения в таком применении.

    Практический пример «каламбура данных»

    Выполнить упаковку или распаковку данных нам может потребоваться при работе с обычными последовательными протоколами связи. Рассмотрим протокол последовательной связи, который отправляет/принимает один байт данных во время каждого сеанса связи. Пока мы работаем с однобайтовыми переменными, данные передавать легко, но что если у нас есть структура произвольного размера, которая должна проходить через канал связи? В этом случае мы должны каким-то образом представить наш объект данных в виде массива переменных размером по одному байту. Затем на стороне получателя мы можем соответствующим образом их упаковать и восстановить исходную структуру.

    Например, предположим, что нам нужно отправить канал связи UART переменную типа float , “ f1 ”. Переменная типа float обычно занимает четыре байта. Следовательно, мы можем использовать следующее объединение в качестве буфера для извлечения четырех байтов “ f1 ”.

    union < float f; struct < uint8_t byte[4]; >; > u1;

    Передатчик записывает переменную “ f1 ” в член объединения с типом float . Затем он считывает массив “ byte ” и отправляет эти байты по каналу связи. Получатель делает обратное: он записывает полученные данные в массив “ byte ” своего собственного объединения и считывает переменную объединения с типом float как полученное значение. Мы могли бы использовать этот метод для передачи объекта данных произвольного размера. Следующий код может использоваться в качестве простого теста для проверки этого метода.

    #include #include int main() < float f1=5.5; union buffer < float f; struct < uint8_t byte[4]; >; >; union buffer buff_Tx; union buffer buff_Rx; buff_Tx.f = f1; buff_Rx.byte[0] = buff_Tx.byte[0]; buff_Rx.byte[1] = buff_Tx.byte[1]; buff_Rx.byte[2] = buff_Tx.byte[2]; buff_Rx.byte[3] = buff_Tx.byte[3]; printf("The received data is: %f", buff_Rx.f); return 0; >

    Рисунок 4, приведенный ниже, визуализирует обсуждаемый метод. Обратите внимание, что байты передаются последовательно.

    Рисунок 4 – Упаковка и распаковка данных при передаче и приеме через последовательный интерфейс

    Заключение

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

    «Каламбур данных» («data punning») или использование объединений для упаковки/распаковки данных может привести к аппаратно-зависимому поведению. Однако его преимущество заключается в том, что оно более читабельно и удобно в поддержке. Вот почему многие программисты предпочитают использовать в этом случае объединения. «Каламбур данных» может быть особенно полезен, когда у нас есть объект данных произвольного размера, который должен передаваться через канал последовательной связи.

    Объединения в С++ (union C++)

    Технология объединений union берет свои истоки в 90-х. Слабенькие по нашим временам ЭВМ (сейчас их и компьютерами то не назовешь), мало памяти (все измерялось килобайтами). Жесткие диски по 40 мегабайт были чуть ли не чудом техники, олицетворяющим огроменные объемы информации, которые умельцами “растачивались” специальными ПО до хранения 80 мегабайт (чтоб побольше было).

    Каждый байт памяти был на вес золота, приходилось экономить на всём. Вот и объединение переменных тоже было призвано (и достаточно успешно, о чем я расскажу ниже) помочь программисту сделать оптимальную и экономную программу, “кушающую” маленькие объемы памяти.

    Чтобы понять в чем смысл объединения нужно вспомнить как хранятся переменные. Разные переменные разного типа или одинаковой группы типов (вроде int , long и short ) несмотря на работу с одним и тем же типом данных (имею ввиду целое) занимают в памяти разное количество байт. long в любом случае занимает максимальное количество байт в памяти, при этом в память для переменной этого типа вполне можно записать значения int или short .

    Просто получится, что не все зарезервированные байты long -а будут востребованы. Если поместить к примеру число 325 в long , будут заняты два байта (зависит от разрядности процессора), а остальные байты заполнятся нулями.

    Именно в этом и появляется смысл union , ибо эта инструкция говорит компилятору: “Зарезервируй мне место для типа данных с максимальным запросом объема памяти, а я уже буду сама разбираться, как и какие значения в них положить”.

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

    Объединения (union) в С++
    short int name1 ;
    long int name3 ;
    > myUnion ; // объект объединения

    Доступ к элементам объединения осуществляется так же, как и к элементам структур: Имя объекта объединения myUnion , точка . и имя элемента name1 .

    К данным, которые хранят элементы структуры (например short, int, long ) мы можем обращаться в любой момент (хоть к одному, хоть и ко всем сразу). А в объединении могут храниться данные либо short , либо int , либо long . Например:

    using namespace std ;
    short int name1 ;
    long int name3 ;
    myUnion . name1 = 22 ;
    cout << myUnion . name1 << endl ; myUnion . name3 = 222222222 ; cout << myUnion . name3 << endl ; cout << myUnion . name1 << endl ; // снова обращаемся к name1

    объединения в с++, union c++, доклад, курсовая работа

    Как видите, после того, как мы записали значение в элемент name3 типа long int , уже невозможно нормально обращаться к элементу name1 . Все потому, что в их общую память уже записано значение long int , а переменная типа short int неспособна работать с данными такого объема. Схематически можно это представить так:

    объединения в с++, union c++, доклад, курсовая работа

    Поэтому, чтобы опять работать с данными типа short int необходимо снова присвоить элементу name1 новое значение. Вот и получается – память одна и та же, а переменные в ней размещаются разные. К какой обращаемся – такая и запрашивает из этой памяти значение.

    Элементы объединения располагаются в памяти начиная с одного места (как бы накладываются друг на друга), в отличии от элементов структуры (они располагаются в памяти последовательно один за другим).

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

    Если бы мы описали просто набор переменных не объединяя их в union , то для их размещения потребовалось бы 2 + 4 + 4 байта = 10 байт. Вот и экономия. А так объединение занимает 4 байта. Целых 6 байт сэкономили, натрамбовав три переменные в один отрезок памяти.

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

    В переменную variant можно без проблем запихнуть до 10 байт (10 это максимум для 32-х разрядных систем под вещественное число). Соответственно в эти 10 байт и все семейство целых и вещественных, и указатели, и объекты, и строки (точнее описатели строк или указатели на них) вмещаются.

    На сегодняшний день экономия памяти не так актуальна, как 15-20 лет назад. Конечно, никто не запрещает использовать union , но зачем? Если есть более удобные на сегодня средства работы с памятью для программиста.

    Следует отметить также, что программы для выполнения в память загружаются с избытком (т.е. для программы выделяется количество памяти, зачастую чрезвычайно большее чем надо). Это можно наблюдать, написав программу с одной переменной. Казалось бы – переменная 4 байта, но открыв диспетчер задач в Винде программа “почему-то” заняла 10 килобайт. Так работает современная операционная система, выделяя памяти с лихвой. В 90-х, увы, такой роскоши и быть не могло.

    В следующем уроке мы рассмотрим битовые поля в С++. Не поленитесь посмотреть видео по теме Объединения (union) в С++:

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

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