На структуры во многом похожи объединения. Объединения (union) также позволяют определить свой тип данных и также хранят набор элементов, но в отличие от структуры все элементы объединения имеют нулевое смещение. А это значит, что разные элементы занимают в памяти один и тот же участок, то есть в памяти они накладываются друг на друга.
Для определения объединений применяется ключевое слово union и следующий формальный синтаксис:
union имя_объединения { тип элемент1; тип элемент2; ............. тип элементN; };
Фактически объединение определяется точно также, как и структура, только вместо слова struct используется ключевое слово union.
Так, создадим простейшее объединение, которое хранит символ и его числовой код из таблицы ASCII:
union ascii { int digit; char letter; };
Объединение ascii хранит в одном и том же участки памяти объект int (числовой код символа) и объект char (сам символ). Конкретный размер выделенной памяти будет зависеть от системы и реализации, но в общем случае это будет выглядеть примерно следующим образом:
В этом случае объединение сode на большинстве платформ будет занимать 4 байта. Длина элементов, как здесь, может быть разной, и в этом случае размер объединения вычисляется по наибольшему элементу.
После определения объединения мы можем создать его переменную и присвоить ей какое-либо значение:
union ascii code;
При определении переменной объединения мы ее можем сразу инициализировать, но стоит учитывать, что инициализировать мы можем только первый элемент объединения. В данном случае это элемент digit типа int, поэтому мы можем передать ему только целое число:
union ascii code = {120};
Для обращения к элементам объединения, как и в случае со структурами, можно использовать операцию "точка":
#include <stdio.h> union ascii { int digit; char letter; }; int main(void) { union ascii code; code.digit = 120; printf("%d - %c \n", code.digit, code.letter); // 120 - x printf("%d - %d \n", code.digit, code.letter); // 120 - 120 code.letter = 87; printf("%d - %c \n", code.digit, code.letter); // 87 - W return 0; }
Здесь создается переменная code, которая представляет объединение ascii. Далее его элементу digit
(числовой код символа) присваивается число 120:
code.digit = 120;
Стоит отметить, что, так как оба элемента - letter и digit занимают одну и ту же память, то данные фактически одни и те же,
только при обращении к code.digit
данные интерпретируются как объект int, а при обращении к code.letter
- как объект char.
printf("%d - %c \n", code.digit, code.letter); // 120 - x printf("%d - %d \n", code.digit, code.letter); // 120 - 120
И изменение одного из них приведет к изменению другого.
code.letter = 87; printf("%d - %c \n", code.digit, code.letter); // 87 - W
Также можно определять анонимные объединения:
#include <stdio.h> union { int digit; char letter; } code1, code2; // переменные code1, code2 int main(void) { code1.digit = 122; code2.digit = 84; printf("%d - %c \n", code1.digit, code1.letter); printf("%d - %c \n", code2.digit, code2.letter); return 0; }
С помощью оператора typedef можно задать псевдоним для объединения:
typedef union ascii { int digit; char letter; } ascii_code;
Здесь псевдонимом является идентификатор ascii_code
, поэтому следующие определения переменных будут аналогичны:
union ascii code = {22}; ascii_code code2 = {22};
Также можно устанавливать псевдоним для анонимных объединений:
typedef union { int digit; char letter; } ascii_code;
Ключевая возможность объединений состоит в том, что они могут применяться для хранения значений разных типов. Выше мы посмотрели, как объединение может хранить данные одновременно типов int и char. Но в подобном случае все несколько просто - int и char накладываются друг на друга - числовой код символа можно выразить и с помощью типа char, и с помощью типа int. Теперь посмотрим более сложный пример:
#include <stdio.h> typedef enum{ NODE_STRING, NODE_INT } node_type; typedef union { int int_value; char* str_value; } node_data; typedef struct{ node_type type; node_data data; } node; void print_node(node n){ if(n.type == NODE_STRING){ printf("String: %s\n", n.data.str_value); } else if(n.type == NODE_INT){ printf("Int: %d\n", n.data.int_value); } } int main(void) { node n1; n1.type = NODE_INT; n1.data.int_value=22; node n2; n2.type = NODE_STRING; n2.data.str_value= "Hello World"; print_node(n1); print_node(n2); return 0; }
Здесь у нас определяется структура node, которая имеет два поля - тип структуры в виде перечисления node_type и непосредственно данные структуры в виде поля node_data.
Перечисление node_type может принимать два значения - NODE_STRING
и NODE_INT
, указывая, что структура будет хранить соответственно или строку,
или целое число. А объединение node_data имеет два поля:
typedef union { int int_value; char* str_value; } node_data;
Если структура представляет число, то для обращения к данным используется поле int_value
, а если строку - то поле str_value
Для упрощения вывода данных структуры node определяем отдельную функцию print_node()
:
void print_node(node n){ if(n.type == NODE_STRING){ printf("String: %s\n", n.data.str_value); } else if(n.type == NODE_INT){ printf("Int: %d\n", n.data.int_value); } }
В зависимости от типа структуры обращаем к полю n.data.str_value
или n.data.int_value
В функции main для тестирования создаем пару структур и выводим их данные на консоль. Консольный вывод программы:
Int: 22 String: Hello World
И как и со структурами, можно определять указатели на объединения. Для обращения к элементам объединения по указателю применяется тот же синтаксис, что и в случае со структурами:
(* указатель_на_объединение).имя_элемента указатель_на_объединение->имя_элемента
Используем указатели на объединения:
#include <stdio.h> union ascii { int digit; char letter; }; int main(void) { union ascii code = {45}; union ascii * p_code = &code; printf("%d \n", p_code->digit); // 45 p_code->digit= 89; printf("%d \n", code.digit); // 89 return 0; }