เมื่อไรควรใช้สหภาพ ทำไมเราต้องการพวกเขา
เมื่อไรควรใช้สหภาพ ทำไมเราต้องการพวกเขา
คำตอบ:
สหภาพมักใช้เพื่อแปลงระหว่างการแทนเลขฐานสองของจำนวนเต็มและลอย:
union
{
int i;
float f;
} u;
// Convert floating-point bits to integer:
u.f = 3.14159f;
printf("As integer: %08x\n", u.i);
แม้ว่านี่จะเป็นพฤติกรรมที่ไม่ได้กำหนดทางเทคนิคตามมาตรฐาน C (คุณควรอ่านเฉพาะฟิลด์ที่เขียนล่าสุด) แต่จะทำงานในลักษณะที่กำหนดไว้อย่างดีในคอมไพเลอร์แทบทุกตัว
สหภาพแรงงานบางครั้งก็ใช้ในการดำเนินการหลอก - polymorphism ใน C โดยให้โครงสร้างบางแท็กระบุประเภทของวัตถุที่มันมีอยู่แล้วสหภาพประเภทที่เป็นไปได้ด้วยกัน:
enum Type { INTS, FLOATS, DOUBLE };
struct S
{
Type s_type;
union
{
int s_ints[2];
float s_floats[2];
double s_double;
};
};
void do_something(struct S *s)
{
switch(s->s_type)
{
case INTS: // do something with s->s_ints
break;
case FLOATS: // do something with s->s_floats
break;
case DOUBLE: // do something with s->s_double
break;
}
}
สิ่งนี้อนุญาตให้มีขนาดเท่ากับstruct S
12 ไบต์แทนที่จะเป็น 28
สหภาพมีประโยชน์อย่างยิ่งในการเขียนโปรแกรมแบบฝังตัวหรือในสถานการณ์ที่ต้องการการเข้าถึงฮาร์ดแวร์ / หน่วยความจำโดยตรง นี่เป็นตัวอย่างเล็กน้อย:
typedef union
{
struct {
unsigned char byte1;
unsigned char byte2;
unsigned char byte3;
unsigned char byte4;
} bytes;
unsigned int dword;
} HW_Register;
HW_Register reg;
จากนั้นคุณสามารถเข้าถึง reg ดังนี้
reg.dword = 0x12345678;
reg.bytes.byte3 = 4;
หลักสูตร Endianness (สั่งไบต์) และโปรเซสเซอร์มีความสำคัญแน่นอน
คุณสมบัติที่มีประโยชน์อีกอย่างคือตัวปรับเปลี่ยนบิต:
typedef union
{
struct {
unsigned char b1:1;
unsigned char b2:1;
unsigned char b3:1;
unsigned char b4:1;
unsigned char reserved:4;
} bits;
unsigned char byte;
} HW_RegisterB;
HW_RegisterB reg;
ด้วยรหัสนี้คุณสามารถเข้าถึงบิตโดยตรงในที่อยู่การลงทะเบียน / หน่วยความจำ:
x = reg.bits.b2;
การเขียนโปรแกรมระบบระดับต่ำเป็นตัวอย่างที่สมเหตุสมผล
IIRC ฉันใช้สหภาพเพื่อแยกส่วนรีจิสเตอร์ฮาร์ดแวร์เป็นบิตคอมโพเนนต์ ดังนั้นคุณสามารถเข้าถึงการลงทะเบียน 8 บิต (เหมือนเดิมในวันที่ฉันทำสิ่งนี้ ;-) ลงในบิตของคอมโพเนนต์
(ฉันลืมไวยากรณ์ที่แน่นอน แต่ ... ) โครงสร้างนี้จะอนุญาตให้เข้าถึงการลงทะเบียนการควบคุมเป็น control_byte หรือผ่านแต่ละบิต มันเป็นสิ่งสำคัญเพื่อให้แน่ใจว่าบิตแมปไปยังบิตการลงทะเบียนที่ถูกต้องสำหรับ endianness ที่กำหนด
typedef union {
unsigned char control_byte;
struct {
unsigned int nibble : 4;
unsigned int nmi : 1;
unsigned int enabled : 1;
unsigned int fired : 1;
unsigned int control : 1;
};
} ControlRegister;
ฉันเคยเห็นมันในห้องสมุดสองแห่งเพื่อแทนที่การสืบทอดเชิงวัตถุ
เช่น
Connection
/ | \
Network USB VirtualConnection
หากคุณต้องการให้การเชื่อมต่อ "คลาส" เป็นแบบใดแบบหนึ่งข้างต้นคุณสามารถเขียนดังนี้:
struct Connection
{
int type;
union
{
struct Network network;
struct USB usb;
struct Virtual virtual;
}
};
ตัวอย่างการใช้ใน libinfinity: http://git.0x539.de/?p=infinote.git;a=blob;f=libinfinity/common/inf-session.c;h=3e887f0d63bd754c6b5ec232948027cbbf4d61fc;hb=HEAD#74
ยูเนี่ยนอนุญาตให้สมาชิกข้อมูลที่ไม่สามารถแบ่งปันหน่วยความจำเดียวกันได้ สิ่งนี้ค่อนข้างสำคัญเมื่อหน่วยความจำขาดแคลนมากเช่นในระบบฝังตัว
ในตัวอย่างต่อไปนี้:
union {
int a;
int b;
int c;
} myUnion;
การรวมนี้จะใช้พื้นที่ของ int เดียวมากกว่า 3 ค่า int ที่แยกจากกัน หากผู้ใช้ตั้งค่าของaและจากนั้นตั้งค่าของbก็จะเขียนทับค่าของaเนื่องจากพวกเขาทั้งสองแบ่งปันตำแหน่งหน่วยความจำเดียวกัน
ประเพณีมากมาย เพียงแค่ทำgrep union /usr/include/*
หรือในไดเรกทอรีที่คล้ายกัน กรณีส่วนใหญ่ที่union
ถูกห่อในstruct
และสมาชิกหนึ่งคนของ struct บอกว่าองค์ประกอบใดในสหภาพที่จะเข้าถึง ตัวอย่างเช่นการชำระเงินman elf
สำหรับการใช้งานในชีวิตจริง
นี่คือหลักการพื้นฐาน:
struct _mydata {
int which_one;
union _data {
int a;
float b;
char c;
} foo;
} bar;
switch (bar.which_one)
{
case INTEGER : /* access bar.foo.a;*/ break;
case FLOATING : /* access bar.foo.b;*/ break;
case CHARACTER: /* access bar.foo.c;*/ break;
}
นี่คือตัวอย่างของการรวมกันจาก codebase ของฉัน (จากหน่วยความจำและการถอดความดังนั้นจึงอาจไม่แน่นอน) มันถูกใช้เพื่อจัดเก็บองค์ประกอบภาษาในล่ามที่ฉันสร้างขึ้น ตัวอย่างเช่นรหัสต่อไปนี้:
set a to b times 7.
ประกอบด้วยองค์ประกอบภาษาต่อไปนี้:
องค์ประกอบภาษาถูกกำหนดเป็น#define
ค่า '' ดังนั้น:
#define ELEM_SYM_SET 0
#define ELEM_SYM_TO 1
#define ELEM_SYM_TIMES 2
#define ELEM_SYM_FULLSTOP 3
#define ELEM_VARIABLE 100
#define ELEM_CONSTANT 101
และโครงสร้างต่อไปนี้ถูกใช้เพื่อจัดเก็บแต่ละองค์ประกอบ:
typedef struct {
int typ;
union {
char *str;
int val;
}
} tElem;
ขนาดของแต่ละองค์ประกอบคือขนาดของยูเนี่ยนสูงสุด (4 ไบต์สำหรับประเภทและ 4 ไบต์สำหรับสหภาพแม้ว่าจะเป็นค่าปกติขนาดจริงขึ้นอยู่กับการนำไปใช้)
ในการสร้างองค์ประกอบ "set" คุณจะต้องใช้:
tElem e;
e.typ = ELEM_SYM_SET;
ในการสร้างองค์ประกอบ "ตัวแปร [b]" คุณจะต้องใช้:
tElem e;
e.typ = ELEM_VARIABLE;
e.str = strdup ("b"); // make sure you free this later
ในการสร้างองค์ประกอบ "ค่าคงที่ [7]" คุณจะต้องใช้:
tElem e;
e.typ = ELEM_CONSTANT;
e.val = 7;
และคุณสามารถขยายเพื่อรวม Floats ( float flt
) หรือ rationals ( struct ratnl {int num; int denom;}
) และประเภทอื่น ๆ
ขั้นพื้นฐานคือการที่str
และval
ไม่ได้อยู่ติดกันในหน่วยความจำที่พวกเขาจริงทับซ้อนกันดังนั้นจึงเป็นวิธีการรับมุมมองที่แตกต่างกันในบล็อกเดียวกันของหน่วยความจำแสดงที่นี่ที่โครงสร้างเป็นไปตามที่ตั้งของหน่วยความจำ0x1010
และจำนวนเต็มและตัวชี้มีทั้ง 4 ไบต์:
+-----------+
0x1010 | |
0x1011 | typ |
0x1012 | |
0x1013 | |
+-----+-----+
0x1014 | | |
0x1015 | str | val |
0x1016 | | |
0x1017 | | |
+-----+-----+
ถ้ามันอยู่ในโครงสร้างมันจะเป็นแบบนี้:
+-------+
0x1010 | |
0x1011 | typ |
0x1012 | |
0x1013 | |
+-------+
0x1014 | |
0x1015 | str |
0x1016 | |
0x1017 | |
+-------+
0x1018 | |
0x1019 | val |
0x101A | |
0x101B | |
+-------+
make sure you free this later
ความคิดเห็นควรถูกลบออกจากองค์ประกอบคงที่หรือไม่
ฉันว่ามันทำให้ง่ายขึ้นในการใช้หน่วยความจำที่อาจถูกนำไปใช้ในรูปแบบที่แตกต่างกันเช่นการบันทึกหน่วยความจำ เช่นคุณต้องการทำโครงสร้าง "ตัวแปร" บางอย่างที่สามารถบันทึกสตริงสั้น ๆ และตัวเลข:
struct variant {
int type;
double number;
char *string;
};
ในระบบ 32 บิตนี้จะส่งผลให้อย่างน้อย 96 บิตหรือ 12 variant
ไบต์ถูกใช้สำหรับอินสแตนซ์ของแต่ละ
การใช้สหภาพคุณสามารถลดขนาดลงเหลือ 64 บิตหรือ 8 ไบต์:
struct variant {
int type;
union {
double number;
char *string;
} value;
};
คุณสามารถประหยัดได้มากขึ้นถ้าคุณต้องการเพิ่มประเภทของตัวแปรที่แตกต่างกัน ฯลฯ มันอาจเป็นจริงได้ว่าคุณสามารถทำสิ่งที่คล้ายกันในการชี้โมฆะได้ - แต่การรวมกันทำให้สามารถเข้าถึงได้ง่ายขึ้นเช่นเดียวกับประเภท ปลอดภัย การประหยัดดังกล่าวไม่ได้เสียงมากนัก แต่คุณประหยัดหน่วยความจำหนึ่งในสามที่ใช้กับอินสแตนซ์ทั้งหมดของโครงสร้างนี้
เป็นการยากที่จะนึกถึงโอกาสเฉพาะเมื่อคุณต้องการโครงสร้างที่ยืดหยุ่นชนิดนี้บางทีในโปรโตคอลข้อความที่คุณจะส่งข้อความที่มีขนาดต่างกัน แต่ถึงอย่างนั้นก็มีทางเลือกที่เป็นมิตรกับโปรแกรมเมอร์มากกว่า
สหภาพเป็นประเภทที่แตกต่างกันเล็กน้อยในภาษาอื่น ๆ - พวกเขาสามารถเก็บได้ครั้งละอย่างเดียว แต่สิ่งนั้นอาจจะเป็น int, float ฯลฯ ขึ้นอยู่กับวิธีที่คุณประกาศ
ตัวอย่างเช่น:
typedef union MyUnion MYUNION;
union MyUnion
{
int MyInt;
float MyFloat;
};
MyUnion จะมีเฉพาะเป็น int หรือลอยขึ้นอยู่กับที่คุณเพิ่งตั้ง ดังนั้นการทำสิ่งนี้:
MYUNION u;
u.MyInt = 10;
ตอนนี้คุณถือ int เท่ากับ 10;
u.MyFloat = 1.0;
ตอนนี้คุณลอยเท่ากับ 1.0 มันไม่ได้มีความตั้งใจ เห็นได้ชัดว่าตอนนี้ถ้าคุณลองทำ printf ("MyInt =% d", u.MyInt); แล้วคุณอาจจะได้รับข้อผิดพลาดแม้ว่าฉันจะไม่แน่ใจในพฤติกรรมที่เฉพาะเจาะจง
ขนาดของสหภาพถูกกำหนดโดยขนาดของสนามที่ใหญ่ที่สุดในกรณีนี้ลอย
sizeof(int) == sizeof(float)
( == 32
) โดยปกติ
สหภาพจะใช้เมื่อคุณต้องการสร้างแบบจำลองโครงสร้างที่กำหนดโดยฮาร์ดแวร์อุปกรณ์หรือโปรโตคอลเครือข่ายหรือเมื่อคุณสร้างวัตถุจำนวนมากและต้องการประหยัดพื้นที่ คุณไม่จำเป็นต้องใช้เวลาถึง 95% ลองใช้รหัสที่ง่ายต่อการตรวจแก้จุดบกพร่อง
หลายคำตอบเหล่านี้เกี่ยวข้องกับการคัดเลือกจากประเภทหนึ่งไปอีกประเภทหนึ่ง ฉันได้รับประโยชน์มากที่สุดจากสหภาพที่มีประเภทเดียวกันมากกว่าพวกเขา (เช่นเมื่อแยกสตรีมข้อมูลอนุกรม) พวกเขาอนุญาตให้แยก / ก่อสร้างแพ็คเก็ตกรอบกลายเป็นเรื่องไม่สำคัญ
typedef union
{
UINT8 buffer[PACKET_SIZE]; // Where the packet size is large enough for
// the entire set of fields (including the payload)
struct
{
UINT8 size;
UINT8 cmd;
UINT8 payload[PAYLOAD_SIZE];
UINT8 crc;
} fields;
}PACKET_T;
// This should be called every time a new byte of data is ready
// and point to the packet's buffer:
// packet_builder(packet.buffer, new_data);
void packet_builder(UINT8* buffer, UINT8 data)
{
static UINT8 received_bytes = 0;
// All range checking etc removed for brevity
buffer[received_bytes] = data;
received_bytes++;
// Using the struc only way adds lots of logic that relates "byte 0" to size
// "byte 1" to cmd, etc...
}
void packet_handler(PACKET_T* packet)
{
// Process the fields in a readable manner
if(packet->fields.size > TOO_BIG)
{
// handle error...
}
if(packet->fields.cmd == CMD_X)
{
// do stuff..
}
}
แก้ไข ความคิดเห็นเกี่ยวกับ endianness และ struct padding นั้นถูกต้องและน่ากังวลมาก ฉันใช้รหัสนี้เกือบทั้งหมดในซอฟต์แวร์ฝังตัวซึ่งส่วนใหญ่ฉันมีการควบคุมปลายทั้งสองของไปป์
สหภาพแรงงานยอดเยี่ยม การใช้สหภาพที่ชาญฉลาดอย่างหนึ่งที่ฉันเคยเห็นคือการใช้พวกมันเมื่อกำหนดเหตุการณ์ ตัวอย่างเช่นคุณอาจตัดสินใจว่าเหตุการณ์เป็น 32 บิต
ตอนนี้ภายใน 32 บิตนั้นคุณอาจต้องการกำหนด 8 บิตแรกสำหรับตัวระบุของผู้ส่งเหตุการณ์ ... บางครั้งคุณจัดการกับเหตุการณ์โดยรวมบางครั้งคุณแยกมันและเปรียบเทียบส่วนประกอบ สหภาพให้ความยืดหยุ่นในการทำทั้งสองอย่าง
กิจกรรมสหภาพ { eventCode แบบยาวที่ไม่ได้ลงชื่อ char eventParts ที่ไม่ได้ลงชื่อ [4]; };
สิ่งที่เกี่ยวกับVARIANT
ที่ใช้ในอินเตอร์เฟส COM? มันมีสองช่อง - "ประเภท" และสหภาพถือค่าจริงที่ถือว่าขึ้นอยู่กับฟิลด์ "ประเภท"
ที่โรงเรียนฉันใช้สหภาพอย่างนี้
typedef union
{
unsigned char color[4];
int new_color;
} u_color;
ฉันใช้มันเพื่อจัดการกับสีได้ง่ายขึ้นแทนที่จะใช้ >> และตัวดำเนินการ << ฉันต้องผ่านดัชนีที่แตกต่างกันของอาร์เรย์ char ของฉัน
ฉันใช้ยูเนี่ยนเมื่อฉันถูกเข้ารหัสสำหรับอุปกรณ์ฝังตัว ฉันมี C int ที่มีความยาว 16 บิต และฉันต้องการเรียก 8 บิตที่สูงกว่าและ 8 บิตที่ต่ำกว่าเมื่อฉันต้องอ่านจาก / store ไปยัง EEPROM ดังนั้นฉันจึงใช้วิธีนี้:
union data {
int data;
struct {
unsigned char higher;
unsigned char lower;
} parts;
};
ไม่จำเป็นต้องเลื่อนเพื่อให้โค้ดอ่านง่ายขึ้น
ในทางกลับกันฉันเห็นโค้ดเก่า ๆ ของ C ++ stl ที่ใช้ union สำหรับ stl allocator หากคุณสนใจคุณสามารถอ่านซอร์สโค้ดsgi stl นี่คือส่วนหนึ่งของมัน:
union _Obj {
union _Obj* _M_free_list_link;
char _M_client_data[1]; /* The client sees this. */
};
struct
รอบhigher
/ ของคุณlower
หรือ ตอนนี้ทั้งสองควรชี้ไปที่ไบต์แรกเท่านั้น
ดูที่นี่: การจัดการคำสั่งบัฟเฟอร์ X.25
หนึ่งในคำสั่ง X.25 ที่เป็นไปได้จำนวนมากได้รับเข้าบัฟเฟอร์และจัดการโดยใช้ UNION ของโครงสร้างที่เป็นไปได้ทั้งหมด
ในรุ่นแรก ๆ ของ C การประกาศโครงสร้างทั้งหมดจะแชร์ชุดของฟิลด์ทั่วไป ได้รับ:
struct x {int x_mode; int q; float x_f};
struct y {int y_mode; int q; int y_l};
struct z {int z_mode; char name[20];};
คอมไพเลอร์จะสร้างตารางขนาดของโครงสร้าง (และอาจจัดตำแหน่ง) และแยกชื่อสมาชิกโครงสร้างและออฟเซ็ตของโครงสร้าง คอมไพเลอร์ไม่ได้ติดตามว่าสมาชิกใดเป็นของโครงสร้างใดและจะอนุญาตให้โครงสร้างสองรายการมีสมาชิกที่มีชื่อเดียวกันเฉพาะเมื่อประเภทและออฟเซ็ตจับคู่ (เช่นเดียวกับสมาชิกq
ของstruct x
และstruct y
) ถ้า p เป็นตัวชี้ไปยังประเภทโครงสร้างใด ๆ p-> q จะเพิ่มออฟเซ็ตของ "q" ไปยังตัวชี้ p แล้วดึง "int" จากที่อยู่ที่ได้
จากความหมายข้างต้นมันเป็นไปได้ที่จะเขียนฟังก์ชั่นที่สามารถปฏิบัติการบางอย่างที่มีประโยชน์กับโครงสร้างหลายชนิดสลับกันได้โดยมีเงื่อนไขว่าทุกฟิลด์ที่ใช้โดยฟังก์ชันเรียงแถวกับฟิลด์ที่มีประโยชน์ภายในโครงสร้างที่เป็นปัญหา นี่เป็นคุณสมบัติที่มีประโยชน์และการเปลี่ยน C เพื่อตรวจสอบความถูกต้องของสมาชิกที่ใช้สำหรับการเข้าถึงโครงสร้างกับประเภทของโครงสร้างที่เป็นปัญหาจะหมายถึงการสูญเสียมันในกรณีที่ไม่มีวิธีการของโครงสร้างที่สามารถมีหลายฟิลด์ที่มีชื่อ การเพิ่มประเภท "union" ลงใน C ช่วยเติมช่องว่างนั้นได้บ้าง (แม้ว่าจะไม่ใช่ IMHO และควรจะเป็น)
ส่วนสำคัญของความสามารถของสหภาพในการเติมช่องว่างนั้นคือข้อเท็จจริงที่ว่าตัวชี้ไปยังสมาชิกสหภาพสามารถแปลงเป็นตัวชี้ไปยังสหภาพใด ๆ ที่มีสมาชิกนั้นและตัวชี้ไปยังสหภาพใด ๆ สามารถแปลงเป็นตัวชี้ไปยังสมาชิกใด ๆ ในขณะที่ C89 มาตรฐานไม่ชัดบอกว่าหล่อT*
โดยตรงไปยังU*
เท่ากับหล่อมันชี้ไปยังสหภาพชนิดใด ๆ ที่มีทั้งสองT
และU
แล้วหล่อที่U*
พฤติกรรมไม่กำหนดลำดับหล่อหลังจะได้รับผลกระทบจาก ประเภทสหภาพใช้และมาตรฐานไม่ได้ระบุความหมายตรงกันข้ามใด ๆ สำหรับโยนตรงจากไปT
U
นอกจากนี้ในกรณีที่ฟังก์ชั่นได้รับตัวชี้แหล่งกำเนิดที่ไม่รู้จักพฤติกรรมของการเขียนวัตถุผ่านT*
แปลงT*
เป็นU*
และจากนั้นการอ่านวัตถุผ่านU*
จะเท่ากับการเขียนสหภาพผ่านสมาชิกของประเภทT
และการอ่านเป็นประเภทU
ซึ่งจะเป็นมาตรฐานที่กำหนดในบางกรณี (เช่นเมื่อเข้าถึงสมาชิกลำดับเริ่มต้นทั่วไป) และกำหนดดำเนินการ (มากกว่าไม่ได้กำหนด ) สำหรับส่วนที่เหลือ ในขณะที่มันเป็นเรื่องยากสำหรับโปรแกรมที่จะใช้ประโยชน์จากการประกัน CIS กับวัตถุจริงของประเภทสหภาพมันเป็นเรื่องธรรมดามากที่จะใช้ประโยชน์จากความจริงที่ว่าตัวชี้ไปยังวัตถุที่ไม่รู้จักแหล่งกำเนิดจะต้องทำตัวเหมือนตัวชี้ไปยังสมาชิกสหภาพ
foo
คือการint
ชดเชย 8 anyPointer->foo = 1234;
หมายถึง "นำที่อยู่ใน anyPointer, แทนที่ด้วย 8 ไบต์และดำเนินการเก็บจำนวนเต็มของค่า 1234 ไปยังที่อยู่ที่ได้รับผลรวบรวมคอมไพเลอร์ไม่จำเป็นต้องรู้หรือสนใจว่าanyPointer
ระบุ โครงสร้างชนิดใดก็ได้ที่foo
ระบุไว้ในหมู่สมาชิก
anyPointer
indentify กับสมาชิก struct แล้วคอมไพเลอร์จะตรวจสอบเงื่อนไขเหล่านี้to have a member with the same name only if the type and offset matched
ของโพสต์ของคุณอย่างไร
p->foo
foo
เป็นหลักได้รับการจดชวเลขp->foo
*(typeOfFoo*)((unsigned char*)p + offsetOfFoo)
สำหรับคำถามหลังของคุณเมื่อคอมไพเลอร์พบคำนิยามสมาชิก struct นั้นต้องการสมาชิกที่ไม่มีชื่อนั้นอยู่หรือสมาชิกที่มีชื่อนั้นมีประเภทและออฟเซตเดียวกัน ฉันเดาว่าจะมีการดักฟังหากคำจำกัดความของสมาชิก struct ไม่ตรงกัน แต่ฉันไม่รู้ว่ามันจัดการข้อผิดพลาดได้อย่างไร
ตัวอย่างที่ง่ายและมีประโยชน์มากก็คือ ....
Imagine:
คุณมีuint32_t array[2]
และต้องการเข้าถึงไบต์ที่ 3 และ 4 ของเครือข่ายไบต์ *((uint16_t*) &array[1])
คุณสามารถทำ แต่สิ่งนี้น่าเศร้าที่ทำลายกฎนามแฝงที่เข้มงวด!
แต่คอมไพเลอร์ที่รู้จักจะอนุญาตให้คุณทำสิ่งต่อไปนี้:
union un
{
uint16_t array16[4];
uint32_t array32[2];
}
ในทางเทคนิคนี่ยังคงเป็นการละเมิดกฎ แต่มาตรฐานที่รู้จักทั้งหมดรองรับการใช้งานนี้