답변:
배열 요소를 구별 된 유니온 (일명 태그 된 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
있습니다.
조합을 사용하십시오.
union {
int ival;
float fval;
void *pval;
} array[10];
그러나 각 요소의 유형을 추적해야합니다.
배열 요소의 크기는 같아야하므로 불가능합니다. 변형 유형 을 만들어서 해결할 수 있습니다 .
#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).
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
. 이 주장 된 문제에 대한 추가 토론의 링크가 있습니까?
연합은 표준적인 방법입니다. 그러나 다른 솔루션도 있습니다. 그중 하나는 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 long
및 uint64_t
...) 태그에 대한 비트가 하나 더 있습니다.
데이터가 다른 곳에 변수에 저장되지 않은 경우 더 많은 메모리가 필요하다는 단점이 있습니다. 따라서 데이터의 유형과 범위가 제한되는 경우 포인터에 직접 값을 저장할 수 있습니다. 이 기술은 32 비트 버전의 Chrome V8 엔진 에서 사용되어 주소의 최하위 비트를 검사하여 다른 객체 (예 : 이중, 큰 정수, 문자열 또는 일부 객체) 또는 31에 대한 포인터 인지 확인합니다. 비트 부호있는 값 ( smi
작은 정수 라고 함 ) 이 경우 int
Chrome은 단순히 산술 오른쪽 시프트 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 비트로 향상 될 때 호환성 문제가 발생했습니다.
x86_64 에서는 상위 비트를 여전히주의해서 태그로 사용할 수 있습니다 . 물론 16 비트를 모두 사용할 필요는 없으며 향후 증거를 위해 일부 비트를 생략 할 수 있습니다.
이전 버전의 Mozilla Firefox에서는 V8과 같은 작은 정수 최적화 를 사용하며 유형 (int, string, object 등) 을 저장하는 데 사용되는 3 개의 하위 비트가 있습니다 . 그러나 JägerMonkey 이후 그들은 또 다른 길을 갔다 ( Mozilla의 New JavaScript Value Representation , backup link ). 값은 항상 64 비트 배정도 변수에 저장됩니다. (가)하면 double
A는 정규화 한, 그것은 계산에 직접적으로 사용될 수있다. 그러나 상위 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