혼합 데이터 유형 (int, float, char 등)을 어떻게 배열에 저장할 수 있습니까?


145

혼합 데이터 형식을 배열에 저장하고 싶습니다. 어떻게 할 수 있습니까?


8
가능하고 사용 사례가 있지만 이는 결함이있는 디자인 일 수 있습니다. 그것은 배열이 아닙니다.
djechlin

답변:


244

배열 요소를 구별 된 유니온 (일명 태그 된 union)으로 만들 수 있습니다 .

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
}

type멤버가 항상에 저장된 마지막 값과 일치 하는지 확인하는 것은 프로그래머에게 맡겨져 union있습니다.


23
+1 이것은 C로 작성된 많은 해석 언어의 함축입니다
texasbruce

8
@texasbruce는 "태그 된 유니온"이라고도합니다. 나는이 기술을 내 언어로도 사용하고 있습니다. ;)

Wikipedia는 세트 이론에서 " 차별화 된 노조 "- "비 연합 "에 대한 명확성 페이지를 사용 하며, @ H2CO3에서 언급했듯이 컴퓨터 과학에서 "태그 된 노조"입니다.
이즈 카타

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 윈도우 시스템에서 이벤트와 같은 것들에 사용되는 스타일입니다.

Barmar의 답변 예제 val는 내부 노조에 이름 을 제공합니다 . Sp.의 답변 예제 .val.는 변형 레코드에 액세스 할 때마다 지정하지 않아도되도록 익명 공용체를 사용합니다 . 불행히도 "익명"내부 구조체 및 공용체는 C89 또는 C99에서 사용할 수 없습니다. 그것은 컴파일러 확장이므로 본질적으로 이식성이 없습니다.

더 나은 방법은 전체 정의를 뒤집는 것입니다. 각 데이터 유형을 자체 구조체로 만들고 태그 (유형 지정자)를 각 구조체에 넣습니다.

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;

대신, 그것은 덜 불쾌한 끝에 간다. :디

이것이 허용하는 또 다른 것은 상속의 형태입니다. 편집 :이 부분은 표준 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;

업 캐스팅 및 다운 캐스팅.


편집 : C99로 지정된 이니셜 라이저로 이들 중 하나를 구성하는 경우 알아야합니다. 모든 멤버 이니셜 라이저는 동일한 통합 멤버를 거쳐야합니다.

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

problem.tag; // may not be initialized

.tag이니셜 라이저는 최적화 컴파일러에서 무시할 수 있습니다 . 이니셜 라이저는 별명 으로 같은 데이터 영역 .int_을 따르기 때문 입니다. 비록 우리가 레이아웃을 (!)을 알고, 그것은 해야 확인합니다. 아니, 그렇지 않아 대신 "internal"태그를 사용하십시오 (원하는 것처럼 외부 태그를 오버레이하지만 컴파일러를 혼동하지 않습니다).

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. 값 에 따라 캐스트하는 함수를 코딩하십시오 .



3

연합은 표준적인 방법입니다. 그러나 다른 솔루션도 있습니다. 그중 하나는 pointer"free" 비트에 더 많은 정보를 저장하는 것을 포함하는 pointer 태그붙습니다 .

아키텍처에 따라 낮은 비트 또는 높은 비트를 사용할 수 있지만 가장 안전하고 이식성이 좋은 방법은 정렬 된 메모리를 활용 하여 사용되지 않은 낮은 비트사용 하는 것입니다. 예를 들어, 32 비트 및 64 비트 시스템에서 포인터 int는 4의 배수 여야int 하고 (32 비트 유형 이라고 가정 ) 최하위 비트는 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 longuint64_t...) 태그에 대한 비트가 하나 더 있습니다.

데이터가 다른 곳에 변수에 저장되지 않은 경우 더 많은 메모리가 필요하다는 단점이 있습니다. 따라서 데이터의 유형과 범위가 제한되는 경우 포인터에 직접 값을 저장할 수 있습니다. 이 기술은 32 비트 버전의 Chrome V8 엔진 에서 사용되어 주소의 최하위 비트를 검사하여 다른 객체 (예 : 이중, 큰 정수, 문자열 또는 일부 객체) 또는 31에 대한 포인터 인지 확인합니다. 비트 부호있는 값 ( smi작은 정수 라고 함 ) 이 경우 intChrome은 단순히 산술 오른쪽 시프트 1 비트를 수행하여 값을 얻습니다. 그렇지 않으면 포인터가 역 참조됩니다.


대부분의 최신 64 비트 시스템에서 가상 주소 공간은 여전히 ​​64 비트보다 훨씬 좁으므로 최상위 비트도 태그로 사용할 수 있습니다 . 아키텍처에 따라 태그로 사용하는 다른 방법이 있습니다. ARM , 68k은 최상위 비트무시 하도록 구성 할 수 있으므로 segfault 등을 걱정하지 않고 자유롭게 사용할 수 있습니다. 위의 링크 된 Wikipedia 기사에서 :

태그가 지정된 포인터를 사용하는 중요한 예는 ARM64의 iOS 7에서 Objective-C 런타임이며, 특히 iPhone 5S에서 사용됩니다. iOS 7에서 가상 주소는 33 비트 (바이트 정렬)이므로 워드 정렬 주소는 30 비트 (3 개의 최하위 비트는 0) 만 사용하므로 태그에는 34 비트가 남습니다. Objective-C 클래스 포인터는 단어로 정렬되며 태그 필드는 참조 카운트 저장 및 오브젝트에 소멸자가 있는지 여부와 같은 여러 목적으로 사용됩니다.

초기 버전의 MacOS는 데이터 객체에 대한 참조를 저장하기 위해 Handles라는 태그가 지정된 주소를 사용했습니다. 어드레스의 상위 비트는 데이터 객체가 각각 리소스 파일로부터 잠금, 제거 가능 및 / 또는 시작되었는지를 표시했다. 이로 인해 MacOS 주소 지정이 시스템 7에서 24 비트에서 32 비트로 향상 될 때 호환성 문제가 발생했습니다.

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

x86_64 에서는 상위 비트를 여전히주의해서 태그로 사용할 수 있습니다 . 물론 16 비트를 모두 사용할 필요는 없으며 향후 증거를 위해 일부 비트를 생략 할 수 있습니다.

이전 버전의 Mozilla Firefox에서는 V8과 같은 작은 정수 최적화 를 사용하며 유형 (int, string, object 등) 을 저장하는 데 사용되는 3 개의 하위 비트가 있습니다 . 그러나 JägerMonkey 이후 그들은 또 다른 길을 갔다 ( Mozilla의 New JavaScript Value Representation , backup link ). 값은 항상 64 비트 배정도 변수에 저장됩니다. (가)하면 doubleA는 정규화 한, 그것은 계산에 직접적으로 사용될 수있다. 그러나 상위 16 비트가 모두 1이고 NaN 을 나타내는 경우 하위 32 비트는 주소 (32 비트 컴퓨터에서)를 값 또는 값으로 직접 저장하고 나머지 16 비트는 사용됩니다. 유형을 저장합니다. 이 기술을 NaN 복싱 이라고합니다또는 수녀 복싱. 64 비트 WebKit의 JavaScriptCore 및 Mozilla의 SpiderMonkey에서도 사용되며 포인터는 하위 48 비트에 저장됩니다. 기본 데이터 유형이 부동 소수점 인 경우 이것이 최상의 솔루션이며 매우 우수한 성능을 제공합니다.

위의 기술에 대해 자세히 읽으십시오 : https://wingolog.org/archives/2011/05/18/value-representation-in-javascript-implementations

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.