ประเภทข้อมูลแบบผสม (int, float, char, ฯลฯ ) จะถูกเก็บไว้ในอาร์เรย์ได้อย่างไร?


145

ฉันต้องการจัดเก็บประเภทข้อมูลแบบผสมในอาเรย์ เราจะทำอย่างนั้นได้อย่างไร?


8
เป็นไปได้และมีกรณีการใช้งาน แต่นี่น่าจะเป็นการออกแบบที่มีข้อบกพร่อง นั่นไม่ใช่สิ่งที่อาร์เรย์มีไว้สำหรับ
djechlin

คำตอบ:


244

คุณสามารถทำให้องค์ประกอบอาร์เรย์สหภาพ discriminated อาคาแท็กยูเนี่ยน

struct {
    enum { is_int, is_float, is_char } type;
    union {
        int ival;
        float fval;
        char cval;
    } val;
} my_array[10];

typeสมาชิกใช้ในการเก็บทางเลือกของการที่สมาชิกของunionถูกควรจะใช้สำหรับแต่ละองค์ประกอบอาร์เรย์ ดังนั้นหากคุณต้องการจัดเก็บintองค์ประกอบแรกคุณจะทำ:

my_array[0].type = is_int;
my_array[0].val.ival = 3;

เมื่อคุณต้องการเข้าถึงองค์ประกอบของอาร์เรย์คุณต้องตรวจสอบประเภทก่อนจากนั้นใช้สมาชิกที่เกี่ยวข้องของสหภาพ switchคำสั่งจะเป็นประโยชน์:

switch (my_array[n].type) {
case is_int:
    // Do stuff for integer, using my_array[n].ival
    break;
case is_float:
    // Do stuff for float, using my_array[n].fval
    break;
case is_char:
    // Do stuff for char, using my_array[n].cvar
    break;
default:
    // Report an error, this shouldn't happen
}

มันเหลือถึงโปรแกรมเมอร์เพื่อให้แน่ใจว่าสมาชิกเสมอสอดคล้องกับค่าสุดท้ายที่เก็บไว้ในtypeunion


23
+1 นี่คือความหมายของการตีความหลายภาษาที่เขียนใน C
texasbruce

8
@ texasbruce หรือที่เรียกว่า "สหภาพที่ติดแท็ก" ฉันใช้เทคนิคนี้ในภาษาของฉันด้วย ;)

Wikipedia ใช้หน้าแก้ความกำกวมสำหรับ " discriminated union " - "disjoint union" ในทฤษฎีเซตและตามที่ @ H2CO3 พูดถึง "tagged union" ในสาขาวิทยาศาสตร์คอมพิวเตอร์
Izkata

14
และบรรทัดแรกของหน้าสหภาพวิกิพีเดียที่ติดแท็กพูดว่า: ในวิทยาการคอมพิวเตอร์, สหภาพที่ติดแท็ก, เรียกอีกอย่างว่าตัวแปร, บันทึกตัวแปร, สหภาพที่แบ่งแยก, สหภาพที่แยกออกจากกัน, หรือกลุ่มผลรวม ...มันได้รับการคิดค้นใหม่หลายครั้ง ชื่อ (ชนิดของพจนานุกรมที่เหมือนกันแฮชอาร์เรย์ที่เชื่อมโยง ฯลฯ )
Barmar

1
@Barmar ฉันเขียนมันใหม่เป็น "สหภาพที่ติดแท็ก" แต่แล้วอ่านความคิดเห็นของคุณ ย้อนกลับไปแก้ไขฉันไม่ได้ตั้งใจที่จะทำลายคำตอบของคุณ

32

ใช้สหภาพ:

union {
    int ival;
    float fval;
    void *pval;
} array[10];

คุณจะต้องติดตามประเภทขององค์ประกอบแต่ละอย่าง


21

องค์ประกอบอาเรย์ต้องมีขนาดเท่ากันนั่นคือสาเหตุที่มันเป็นไปไม่ได้ คุณสามารถแก้ไขได้ด้วยการสร้างประเภทชุดตัวเลือก :

#include <stdio.h>
#define SIZE 3

typedef enum __VarType {
  V_INT,
  V_CHAR,
  V_FLOAT,
} VarType;

typedef struct __Var {
  VarType type;
  union {
    int i;
    char c;
    float f;
  };
} Var;

void var_init_int(Var *v, int i) {
  v->type = V_INT;
  v->i = i;
}

void var_init_char(Var *v, char c) {
  v->type = V_CHAR;
  v->c = c;
}

void var_init_float(Var *v, float f) {
  v->type = V_FLOAT;
  v->f = f;
}

int main(int argc, char **argv) {

  Var v[SIZE];
  int i;

  var_init_int(&v[0], 10);
  var_init_char(&v[1], 'C');
  var_init_float(&v[2], 3.14);

  for( i = 0 ; i < SIZE ; i++ ) {
    switch( v[i].type ) {
      case V_INT  : printf("INT   %d\n", v[i].i); break;
      case V_CHAR : printf("CHAR  %c\n", v[i].c); break;
      case V_FLOAT: printf("FLOAT %f\n", v[i].f); break;
    }
  }

  return 0;
}

ขนาดขององค์ประกอบของสหภาพคือขนาดขององค์ประกอบที่ใหญ่ที่สุด 4


8

มีลักษณะที่แตกต่างกันในการกำหนดแท็ก - ยูเนี่ยน (ตามชื่ออะไรก็ตาม) ที่ IMO ใช้ให้ดีกว่าการใช้โดยการลบสหภาพภายใน นี่คือสไตล์ที่ใช้ในระบบ X Window สำหรับสิ่งต่างๆเช่นกิจกรรม

ตัวอย่างในคำตอบของ Barmar ให้ชื่อvalแก่สหภาพภายใน ตัวอย่างในคำตอบของ Sp. ใช้ยูเนี่ยนนิรนามเพื่อหลีกเลี่ยงการระบุ.val.ทุกครั้งที่คุณเข้าถึงเร็กคอร์ดชุดตัวเลือก น่าเสียดายที่โครงสร้างแบบไม่ระบุชื่อและสหภาพภายในไม่สามารถใช้งานได้ใน C89 หรือ C99 มันเป็นส่วนขยายของคอมไพเลอร์และดังนั้นจึงไม่ใช่แบบพกพา

วิธีที่ดีกว่า IMO คือการกลับคำจำกัดความทั้งหมด ทำให้แต่ละชนิดข้อมูลมีโครงสร้างของตนเองและใส่แท็ก (ตัวระบุชนิด) ลงในแต่ละโครงสร้าง

typedef struct {
    int tag;
    int val;
} integer;

typedef struct {
    int tag;
    float val;
} real;

จากนั้นคุณห่อสิ่งเหล่านี้ในสหภาพระดับบนสุด

typedef union {
    int tag;
    integer int_;
    real real_;
} record;

enum types { INVALID, INT, REAL };

ตอนนี้มันอาจดูเหมือนว่าเรากำลังทำซ้ำตัวเองและเรามี แต่พิจารณาว่าคำจำกัดความนี้มีแนวโน้มที่จะแยกเป็นไฟล์เดียว แต่เราได้ขจัดเสียงรบกวนของการระบุสื่อกลาง.val.ก่อนที่คุณจะได้รับข้อมูล

record i;
i.tag = INT;
i.int_.val = 12;

record r;
r.tag = REAL;
r.real_.val = 57.0;

แต่มันกลับกลายเป็นสิ่งที่น่ารังเกียจน้อยกว่า : D

สิ่งนี้ช่วยให้เป็นรูปแบบของการสืบทอด แก้ไข: ส่วนนี้ไม่ได้มาตรฐาน C แต่ใช้ส่วนขยาย GNU

if (r.tag == INT) {
    integer x = r;
    x.val = 36;
} else if (r.tag == REAL) {
    real x = r;
    x.val = 25.0;
}

integer g = { INT, 100 };
record rg = g;

หล่อขึ้นและลงหล่อ


แก้ไข: gotcha หนึ่งอันที่ต้องระวังคือถ้าคุณสร้างหนึ่งในสิ่งเหล่านี้ด้วย initializers ที่กำหนดไว้ C99 สมาชิกเริ่มต้นทั้งหมดควรผ่านสมาชิกสหภาพเดียวกัน

record problem = { .tag = INT, .int_.val = 3 };

problem.tag; // may not be initialized

.taginitializer สามารถปฏิเสธโดยคอมไพเลอร์เพิ่มประสิทธิภาพเพราะ.int_การเริ่มต้นที่ตามนามแฝงพื้นที่ข้อมูลเดียวกัน ถึงแม้ว่าเราจะรู้ว่ารูปแบบ (!) และมันควรจะ ok ไม่มันไม่ใช่ ใช้แท็ก "ภายใน" แทน (จะซ้อนทับแท็กด้านนอกเหมือนกับที่เราต้องการ แต่ไม่สับสนกับคอมไพเลอร์)

record not_a_problem = { .int_.tag = INT, .int_.val = 3 };

not_a_problem.tag; // == INT

.int_.valไม่นามแฝงพื้นที่เดียวกัน แต่เพราะคอมไพเลอร์รู้ว่าที่มากขึ้นกว่าชดเชย.val .tagคุณมีลิงค์สำหรับการอภิปรายเพิ่มเติมเกี่ยวกับปัญหาที่ถูกกล่าวหานี้หรือไม่?
MM

5

คุณสามารถทำvoid *อาเรย์กับอาเรย์ที่แยกจากกันsize_t.แต่คุณสูญเสียชนิดข้อมูล
หากคุณต้องการเก็บชนิดข้อมูลในทางใดทางหนึ่งให้เก็บอาร์เรย์ที่สามของ int (โดยที่ int เป็นค่าที่แจกแจง) จากนั้นให้โค้ดฟังก์ชันที่ใช้งานขึ้นอยู่กับenumค่า


คุณยังสามารถเก็บข้อมูลประเภทไว้ในตัวชี้ได้ด้วย
phuclv

3

ยูเนี่ยนเป็นวิธีมาตรฐานในการเดินทาง แต่คุณมีวิธีแก้ไขปัญหาอื่น ๆ เช่นกัน หนึ่งในนั้นคือแท็กตัวชี้ซึ่งเกี่ยวข้องกับการจัดเก็บข้อมูลเพิ่มเติมในบิต"ฟรี"ของตัวชี้

ขึ้นอยู่กับสถาปัตยกรรมที่คุณสามารถใช้บิตต่ำหรือสูง แต่วิธีที่ปลอดภัยและพกพามากที่สุดคือการใช้บิตต่ำที่ไม่ได้ใช้โดยใช้ประโยชน์จากหน่วยความจำที่จัดตำแหน่ง ตัวอย่างเช่นในระบบ 32- บิตและ 64- บิตพอยน์เตอร์จะintต้องเป็นทวีคูณของ 4 (สมมติว่าintเป็นประเภท 32- บิต) และ 2 บิตที่สำคัญน้อยที่สุดต้องเป็น 0 ดังนั้นคุณสามารถใช้พวกเขาเพื่อเก็บประเภทของค่าของคุณ . แน่นอนคุณจำเป็นต้องล้างแท็กบิตก่อนที่จะทำการอ้างอิงตัวชี้อีกครั้ง ตัวอย่างเช่นหากประเภทข้อมูลของคุณถูก จำกัด ไว้ที่ 4 ประเภทที่แตกต่างกันคุณสามารถใช้มันได้ด้านล่าง

void* tp; // tagged pointer
enum { is_int, is_double, is_char_p, is_char } type;
// ...
uintptr_t addr = (uintptr_t)tp & ~0x03; // clear the 2 low bits in the pointer
switch ((uintptr_t)tp & 0x03)           // check the tag (2 low bits) for the type
{
case is_int:    // data is int
    printf("%d\n", *((int*)addr));
    break;
case is_double: // data is double
    printf("%f\n", *((double*)addr));
    break;
case is_char_p: // data is char*
    printf("%s\n", (char*)addr);
    break;
case is_char:   // data is char
    printf("%c\n", *((char*)addr));
    break;
}

หากคุณมั่นใจได้ว่าข้อมูลมีการจัดตำแหน่งแบบ 8 ไบต์ (เช่นตัวชี้ในระบบ 64 บิตหรือlong longและuint64_t... ) คุณจะมีแท็กอีกหนึ่งบิต

นี่เป็นข้อเสียอย่างหนึ่งที่คุณต้องการหน่วยความจำเพิ่มเติมหากข้อมูลไม่ได้ถูกเก็บไว้ในตัวแปรที่อื่น ดังนั้นในกรณีที่ประเภทและช่วงข้อมูลของคุณมี จำกัด คุณสามารถจัดเก็บค่าได้โดยตรงในตัวชี้ เทคนิคนี้ใช้ในเครื่องมือ V8 ของ Chromeรุ่น 32 บิตซึ่งจะตรวจสอบที่อยู่บิตที่สำคัญน้อยที่สุดเพื่อดูว่าเป็นตัวชี้ไปยังวัตถุอื่น (เช่นคู่จำนวนเต็มใหญ่สตริงหรือวัตถุบางอย่าง) หรือ31 - ค่าบิตที่เซ็นชื่อ (เรียกว่าsmi- จำนวนเต็มขนาดเล็ก ) หากเป็นintเช่นนั้น Chrome จะทำการเปลี่ยนแปลงทางคณิตศาสตร์อย่างถูกต้อง 1 บิตเพื่อรับค่ามิฉะนั้นตัวชี้จะถูกยกเลิกการลงทะเบียน


ในระบบ 64 บิตปัจจุบันพื้นที่ที่อยู่เสมือนนั้นยังแคบกว่า 64 บิตดังนั้นบิตที่มีความสำคัญสูงสุดสามารถใช้เป็นแท็กได้ ขึ้นอยู่กับสถาปัตยกรรมคุณมีวิธีใช้แท็กต่างกัน สามารถกำหนดค่าARM , 68kและอื่น ๆ อีกมากมายให้ละเว้นบิตอันดับต้นๆ ทำให้คุณสามารถใช้งานได้อย่างอิสระโดยไม่ต้องกังวลกับ segfault หรืออะไรก็ตาม จากบทความ Wikipedia ที่ลิงก์ด้านบน:

ตัวอย่างที่สำคัญของการใช้ตัวชี้ที่ติดแท็กคือรันไทม์ Objective-C บน iOS 7 บน ARM64 โดยเฉพาะอย่างยิ่งที่ใช้บน iPhone 5S ใน iOS 7 ที่อยู่เสมือนมี 33 บิต (จัดเรียงตามแนวไบท์) ดังนั้นที่อยู่ที่จัดเรียงคำจะใช้ 30 บิตเท่านั้น (3 บิตที่สำคัญน้อยที่สุดคือ 0) เหลือ 34 บิตสำหรับแท็ก พอยน์เตอร์คลาส C ของ Objective จะจัดตำแหน่งคำและฟิลด์แท็กจะถูกใช้เพื่อวัตถุประสงค์หลายอย่างเช่นการจัดเก็บจำนวนการอ้างอิงและวัตถุมี destructor หรือไม่

MacOS เวอร์ชันก่อนหน้านี้ใช้ที่อยู่ที่ติดแท็กชื่อ Handle เพื่อจัดเก็บการอ้างอิงไปยังวัตถุข้อมูล บิตสูงของที่อยู่ที่ระบุว่าวัตถุข้อมูลถูกล็อค, ล้างทำความสะอาดได้และ / หรือมาจากไฟล์ทรัพยากรตามลำดับ สิ่งนี้ทำให้เกิดปัญหาความเข้ากันได้เมื่อ MacOS แอดเดรสขั้นสูงจาก 24 บิตเป็น 32 บิตในระบบ 7

https://en.wikipedia.org/wiki/Tagged_pointer#Examples

เมื่อวันที่ x86_64 คุณยังสามารถใช้บิตที่สูงที่สุดเท่าที่แท็กด้วยความระมัดระวัง แน่นอนคุณไม่จำเป็นต้องใช้ 16 บิตเหล่านั้นทั้งหมดและสามารถทิ้งบิตไว้เพื่อใช้เป็นหลักฐานในอนาคต

ใน Mozilla Firefox เวอร์ชันก่อนหน้าพวกเขายังใช้การเพิ่มประสิทธิภาพจำนวนเต็มขนาดเล็กเช่น V8 ด้วย3 บิตต่ำที่ใช้ในการจัดเก็บประเภท (int, string, object ... ฯลฯ ) แต่เนื่องจากJägerMonkeyพวกเขาใช้เส้นทางอื่น ( การแทนค่า JavaScript ใหม่ของ Mozilla , ลิงก์สำรอง ) ค่านี้จะถูกเก็บไว้ในตัวแปรความแม่นยำสองเท่าแบบ 64 บิตเสมอ เมื่อdoubleเป็นค่าที่ถูกทำให้เป็นมาตรฐานมันสามารถใช้โดยตรงในการคำนวณ อย่างไรก็ตามถ้า 16 บิตสูงของมันคือ 1s ทั้งหมดซึ่งหมายถึงNaN , 32 บิตต่ำจะเก็บที่อยู่ (ในคอมพิวเตอร์ 32 บิต) เป็นค่าหรือค่าโดยตรง 16 บิตที่เหลือจะถูกนำมาใช้ เพื่อจัดเก็บประเภท เทคนิคนี้เรียกว่าNaN-Boxingหรือแม่ชีมวย มันยังใช้ใน JavaScriptCore 64 บิตของ WebKit และ SpiderMonkey ของ Mozilla ด้วยตัวชี้ที่ถูกจัดเก็บใน 48 บิตต่ำ หากประเภทข้อมูลหลักของคุณเป็นทศนิยมจุดนี้เป็นทางออกที่ดีที่สุดและให้ประสิทธิภาพที่ดีมาก

อ่านเพิ่มเติมเกี่ยวกับเทคนิคด้านบน: https://wingolog.org/archives/2011/05/18/value-representation-in-javascript-implementations

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.