Если вам трудно понять как действует объеднение union в С++, вспомните игру "Царь горы". Представьте, что в эту игру играют переменные и каждая переменная хочет на верхушку горы поставить свой собственный флаг, своё значение. Как только одна ставит свой флаг, она как герой кому-то нужна, её значение используется программой, а потом её опрокидывает другой участник игры, другая переменная, выкидывает флаг первой и выставляет свой, теперь она, вторая переменная, — герой, она нужна, её значение используется. И этот процесс происходит до конца работы программы. Участников много, а используется только один, только когда нужен. Какая переменная используется — определяется программистом.
Классы, структуры, перечисления и объединения — всё относится к группировке переменных. Класс и структура в С++ отличаются только модификатором доступа по умолчанию (скрывать по умолчанию внутренние данные или не скрывать), перечисления отличаются от классов и структур тем, что хранении только константы, а объединения отличаются от всего этого тем, что имеют в своём описании несколько переменных/объектов, а использовать в один момент времени возможно только одну сущность из всех.
Объединения — это структура данных, члены которой расположены по одному и тому же адресу. Поэтому размер объединения равен размеру его наибольшего члена. В любой момент времени объединение хранит значение только одного из членов.
Было время, когда мне было сложно уяснить, что таится под таинственным "В любой момент времени объединение хранит значение только одного из членов". Так вот, попытаюсь вам объяснить этот момент максимально просто. Представьте себе, что у вас есть одноместное купе поезда, а переменные — это люди: солидный мужчина толстяк, красивая молодая девушка и ребёнок. В купе поезда может поместиться каждый из них, но одновременно никто из них рядом с другим находится не может (обида ли это, личная ли неприязнь или что-то другое — не особо важно, важно только что в одном купе никто из них с другим быть не может). Так вот, когда одна личность заходит в купе, другая сразу же из купе выходит. По такому приблизительно принципу и работает объединение union в С++. В нашем случае купе — это адрес памяти, а каждая личность определённый тип (по аналогии может быть так: солидный мужчина толстяк — это int, красивая молодая девушка — это float и ребёнок — это char). Используются объединения для экономии памяти. В современных домашних ПК достаточно много памяти и поэтому дефицит памяти может быть сложно представить, но кроме домашних ПК есть много других компьютеров: Язык C++ также применяется для встроенных систем, таких как процессоры, управляющие духовым шкафом, МРЗ-проигрывателем или марсоходом. В таких приложениях пространство может быть дефицитным ресурсом. Кроме того, объединения часто используются в мультимедийных библиотеках и при работе с операционными системами или аппаратными структурами данных. В современных реалиях в повседневном коде структура вида union особо не нужна, есть ей альтернатива в виде std::variant. Но поскольку вы вполне можете встретить в кодах объединение union, да и просто для саморазвития знакомство с объединениями union С++ для вас может оказаться полезным.
Объединения разделяются на обычные объединения и анонимные объединения.
Эти объединения имеют свои собственные особенности. Дальше в моей статье пойдёт много примеров и описание особенностей объединений. Я надеюсь, что непосредственно как оно работает, теперь вы легко себе представляете. Можно условно считать, что внутри объединений union собираются переменные-грубияны, которые хамски выталкивают всё, что рядом лежит, оставляя только себя в гордом одиночестве за нужностью себя программисту.
В основе этой темы заложены материалы из книг преимущественно описывающих С++11, у меня могут быть некоторые неточности для более старых компиляторов, либо я что-то пропущу (но если бы знал, то не пропускал бы конечно).
Начнём с изучения обычных объединений. Использование объединения обозначает, что имеется намерение задействовать только одну ячейку памяти компьютера, в которую будут подставляться значения переменных, объединённых в одну группу и вероятно имеющих разные типы. Типы в С++ ни что иное как подсказка компьютеру, сколько байтов памяти нужно отвести под переменную. Самый широкий тип, описанный внутри объединения, считается за подсказку, сколько памяти нужно выделить объединению. Объединения подобны структурам и пишутся объединения также как и структуры.
C++
1
2
3
4
5
6
//Листинг #1.1 Создание union
unionMyUnion{
charx;
inty;
longz;
};
Как вы можете видеть, в создании простого объединения ничего сложного нет. Под размером класса (class) или структуры (struct) подразумевается размер объекта, тип которого соответствует классу или структуре. Размер такого объекта обычно можно или посчитать точно или понять приблизительно, если сложить размеры всех типов, заложенных в класс или структуру. В отличие от классов и структур, размер объединения определяется самым широким типом, заложенным в объединение. Ширина типа — это ёмкость типа: тип int обычно шире, чем short, а long int обычно шире, чем int. Т. е. чем большее число показывает sizeof для типа, тем шире тип.
Поскольку внутри объединения MyUnion самый широкий тип long, то ради переменных, сгруппированных в объединении MyUnion, будет выделено памяти столько же, сколько памяти выделяется для переменной типа long. Это одна ячейка памяти, достаточно большая для того, чтобы вместить вовнутрь себя значение любой переменной из MyUnion. Поскольку адрес один, то действия над любой сгруппированной в MyUnion переменной будут затрагивать значение, попавшее вовнутрь выделенной ячейки.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//Листинг #2.1 union Пример использования
#include <iostream>
unionMyUnion
{
shortx;//для x тип short
inty;//для y типом int
};
intmain()
{
usingnamespacestd;
MyUnionu;//Создаём объект u объединения MyUnion
u.x=200;//Записываем в u.x Значение 200
u.y=111;//Записываем в u.y значение 111
cout<<u.x;//u.x было переписано последней записью, поскольку всё пишется в одну ячейку памяти
}
Как вы можете увидеть по листингу #2.1, любое изменение, независимо от выбранной переменной из объединения буквально выталкивает уже сохранённое значение. Несложно догадаться, что на одном адресе можно хранить только что-то одно, поэтому происходит своеобразная перепись с затиранием. Поскольку для записи используется одна ячейка памяти, происходит экономия байтов. Байт — это единица информации. Для структуры с тем же описанием, что у MyUnion могло бы потребоваться (>= short + int) байтов в то время, когда для непосредственно нашего MyUnion нужно только int байтов.
В языке С++ переменные могут быть статическими static и нестатическими. Внутри обычных объединений можно создавать статические переменные. Поскольку статические переменные, объявляемые внутри классов (соответственно и внутри объединений), по природе своей самые обычные глобальные переменные, то для статических переменных правило затирания не сработает: статические переменные внутри объединений друг другу не мешают. Для них используются отдельные адреса. Своеобразные ВИП-клиенты объединения (со своими капризами).
C++
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
//Листинг #3.1 Использование статических переменных внутри union
#include <iostream>
unionMyUnion
{
staticcharch1;
staticcharch2;
shortx;//для x тип short
longy;//для y типом int
};
charMyUnion::ch1='2';
charMyUnion::ch2='9';//Записываем в u.y значение 111
intmain()
{
usingnamespacestd;
MyUnionu;//Создаём объект u объединения MyUnion
MyUnion::ch1='2';//Записываем в ch1 символ '2'
MyUnion::ch2='9';//Записываем в ch2 символ '9'
cout<<MyUnion::ch1<<'\n';//Не затёрлось, 2
cout<<MyUnion::ch2<<'\n';//Не затёрлось, 9
}
Для обычных объединений дейтвуют следующие ограничения:
Объединение может содержать функции-члены (в том числе конструкторы и деструкторы), но не может содержать виртуальные функции.
Объединение не должно иметь базовых классов.
Объединение не может выступать в качестве базового класса.
Если объединение содержит нестатический данное-член ссылочного типа, программа считается неправильной.
До С++11 нельзя было использовать объекты с нетривиальными конструкторами в объединениях.
После С++11 если нестатический данное-член объединения имеет нетривиальный конструктор умолчания, конструктор копирования, конструктор перемещения, операцию присваивания копированием, операцию присваивания перемещением или деструктор, то соответствующая функция-член объединения должна быть предоставленной-пользователем, либо она будет неявно описана в объединении как удаленная
К сожалению, в пункте ограничений, где пользователь должен предоставлять соответствующие функции-члены объединения, мой мозг сопротивляется, поэтому я не могу показать собственный пример, могу показать тот, что мне показали, но не смогу его описать. Если случится чудо и мой мозг наконец решит понять тему, то я непременно опишу этот момент, а пока что оставлю в этой статье пробелом.
Пока что могу пояснить очень малое. Это имеет отношение к С++11 и старше.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Листинг #4.1 union Объект с нетривиальным конструктором в объединении
Так как тип std :: string объявляет нетривиальные версии всех своих специальных функций-членов, объединение MyUnion будет иметь неявный удаленный конструктор умолчания, конструктор копирования и перемещения, операцию присваивания копированием и перемещением и деструктор. Чтобы использовать MyUnion, некоторые или все эти функции-члены должны быть предоставленными-пользователем.
Об обычных объединениях поговорили, почитали. Теперь переходим к анонимным объединениям. Анонимные объединения — это те же объединения, но не имеющие ни названия, ни порождённых от себя объектов. Такие объединения имеют некоторые собственные особенности.
Внутри анонимного объединения должны группироваться только нестатические данные-члены.
В анонимном объединении не могут объявляться вложенные типы, анонимные объединения и функции. Хотя компиляторы могут компилировать код с анонимными объединениями, вложеннными в анонимные объединения, это нарушает правила языка С++.
Имена членов анонимного объединения должны отличаться от имен любой другой сущности в области действия, в которой объявлено анонимное объединение.
Анонимные объединения, объявленные в именованном пространстве имен или в глобальном пространстве имен, должны быть объявлены как статические.
Анонимные объединения, объявленные в области действия блока, должны быть объявлены с любым классом памяти, допустимом для переменных из области действия блока, или объявлены без указания класса памяти.
В объявлении анонимного объединения в области действия класса не допускается задание класса памяти.
Анонимное объединение не должно иметь скрытые или защищенные члены
Анонимное объединение не должно содержать функции-члены.
Если для объединения объявлены объекты, указатели или ссылки, то оно не считается анонимным объединением.
Есть ещё и такое понятие, как класс, подобный объединению (union-like class). Этим термином называют объединение или класс, имеющий в качестве непосредственного члена анонимное объединение. Класс X, подобный объединению, имеет множество вариантных членов (variant members).
Теперь посмотрим примеры:
C++
1
2
3
4
5
6
7
8
9
10
//Листинг #6.1 Анонимные объединения
structMyStruct{
union{//У объединения нет ни имени, ни порождённых объектов, объединение анонимно
intx;
doubley;
};
};
intmain(){
}
C++
1
2
3
4
5
6
7
8
9
10
11
//Внутри анонимного объединения должны группироваться только нестатические данные-члены.
//Листинг #7.1 Анонимные объединения
structMyStruct{
union{//У объединения нет ни имени, ни порождённых объектов, объединение анонимно
intx;
staticdoubley;//статическая переменная в анонимном объединении не допускается, ошибка компиляции
};
};
intmain(){
}
Внутри анонимных объединений нельзя размещать вложенные типы, анонимные объявления и функции.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Внутри анонимного объединения не может быть классов, анонимных объявлений и функций.
//Листинг #8.1 Анонимные объединения
structMyStruct{
union{//У объединения нет ни имени, ни порождённых объектов, объединение анонимно
structMyStruct{
intx;//вложенный тип в анонимном объединении не допускается, ошибка компиляции
}
voidfoo(){}//функции в анонимном объединении не допускаются, ошибка компиляции
};
};
intmain(){
}
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//Внутри анонимного объединения не может быть классов, анонимных объявлений и функций.
//Листинг #8.2 Анонимные объединения
structMyStruct{
union{//У объединения нет ни имени, ни порождённых объектов, объединение анонимно
intx;
union{//У объединения нет ни имени, ни порождённых объектов,
floaty;//объединение анонимно и вложено в анонимное объединение, не допускается
};
};
};
intmain(){
}
Хотя листинг #8.2 может вполне успешно скомпилироваться, а программа заработать, правила С++ явно запрещают это проделывать.
У анонимных объявлений есть такая особенность, что использовать вытаскиваемую из группы собранных внутри объединения переменных можно так, как будто вытащенная переменная не относится к объединению, т. е. использовать такие переменные можно напрямую.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//Листинг #9.1 Анонимные объединения Использование переменной из анонимного объединения
#include <iostream>
usingstd::cout;
structMyStruct{
union{//У объединения нет ни имени, ни порождённых объектов, объединение анонимно
intx;
};
};
intmain(){
MyStructO;//Объект структуры
O.x=100;
cout<<O.x<<'\n';//Анонимное объединение не требует дополнительного доступа
}
Чуть позднее будет показан более интуитивно-понятный пример.
C++
1
2
3
4
5
6
7
8
9
10
11
//Листинг #9.2 Анонимные объединения Использование одного названия для 2 переменных
structMyStruct{
union{//У объединения нет ни имени, ни порождённых объектов, объединение анонимно
intx;
};
floatx;//имя x уже занято, ошибка компиляции
};
intmain(){
}
Анонимное объединение можно в некотором роде рассматривать так, как будто его нет, как будто любая объявляемая внутри объединения переменная напрямую объявляется внутри оласти видимости, в которой находится само анонимное объединение. Тем не менее, не стоит забывать, что для любых нестатических переменных, собранных внутри объединения, будет действовать правило записи на один адрес. Т. е. переменная из объединения компилятором рассматривается как переменная из объединения, а нами может быть написана как самостоятельная переменная во время использования её.
Если анонимное объединение создаётся как глобальное (или вне класса, но внутри пространства имён, наподобие глобального), то такое объединение нужно объявлять статическим.
C++
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
33
34
35
//Листинг #10.1 Анонимные объединения
#include <iostream>
/*Объединение внутри пространства имён s*/
namespaces{
staticunion{//У объединения нет имени, оно анонимно, попробуйте без static
intx;
doubley;
};
}
/*Анонимное объединение в глобальном пространстве имён*/
staticunion{////У объединения нет имени, оно анонимно, попробуйте без static
intx;
inty;
};
intmain(){
usingstd::cout;
/*Используем переменную из анонимного объединения*/
x=100;
cout<<"x = "<<x<<'\n';
y=200;
cout<<"y = "<<y<<'\n';
/*Используем переменную из анонимного объединения*/
s::x=100;
cout<<"x = "<<s::x<<'\n';
s::y=200;
cout<<"y = "<<s::y<<'\n';
}
Как вы можете видеть, переменная, вытаскиваемая из объединения используется как будто это независимая переменная. По этой причине там, где напрямую доступно название переменной объединения, нельзя задать таким же названием любую другую сущность.
Анонимные объединения, объявленные в области действия блока, должны быть объявлены с любым классом памяти, допустимом для переменных из области действия блока, или объявлены без указания класса памяти.
C++
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
//листинг #11.1 Добавляем класс памяти к анонимному объединению, локализованному вовнутрь блоков
#include <iostream>
intfoo(){
registerunion{
intx;//Добавили автоматический класс памяти
};
}
intmain(){
{
staticunion{//Добавили статический класс памяти
intx;
};
registerunion{//Добавили автоматический класс памяти
inty;
};
thread_localunion{//Добавили потоковый класс памяти
intz;
};
}
}
Поскольку в С++11 ключевое слово auto получило новый смысл своего существования, я использую ключевое слово register. Это тоже добавление переменной автоматического класса памяти (их два вида, автоматических переменных: auto и register. Если ничего не писать, то по-умолчанию используется вид auto, если в C++11 или выше писать auto, то это обозначает выведение типа, а не придание класса памяти, поэтому я задействую второй вариант, чтобы было видно само слово по факту. Описание классов памяти выходит за рамки этой статьи, здесь просто отображается, что так делать вполне законно.
В отличие от подобных себе анонимное объединение не позволяет внутри себя использовать ключевые слова private и protected.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//Листинг #12.1 Анонимные объединения Внутри анонимных объединений нельзя private и protected
#include <iostream>
usingstd::cout;
structMyStruct{
union{
public:
intx;//OK
private:
doubley;//Ошибка!
};
};
intmain(){
}
Кроме того, в анонимном объединении не может быть функций.
C++
1
2
3
4
5
6
7
8
9
10
11
12
//Листинг #13.1 Анонимные объединения Внутри анонимных объединений нельзя описывать функции
#include <iostream>
usingstd::cout;
structMyStruct{
union{
voidfoo(){}//Ошибка!
};
};
intmain(){
}
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//Листинг #14.1 Если от неименованного объединения порождены объекты, то такое объединение не анонимно
#include <iostream>
usingstd::cout;
voidf(){
union{
intaa;char*p;
}obj,*ptr=&obj;
// aa = 1; // ошибка
ptr->aa=1;//OK
}
intmain(){
}
Как вы можете заметить в примере листинга #14.1, поскольку объединение не анонимно, у нас нет прямого доступа к внутренней переменной.
В С++11 разрешили использовать инициализацию прямо внутри объединений (не только объединений, но эта статья об объединениях), так вот такую инициализацию можно производить только для одной переменной из всей группы переменных объединения.
Если класс или объединение внутри себя непосредственно сразу содержит анонимное объединение, то такой класс называется классом, подобным объединению. Такие классы имеют множество так называемых вариативных членов.
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//Листинг #16.1 Анонимные объединения Только один вариативный член разрешено инициализировать
unionMyUnion{
intx=0;
union{
intk;
};
union{
intz;
inty=1;// ошибка: инициализация второго вариантного члена MyUnion
extern — это способ линковки (external linkage), а не класс памяти.
Спасибо.