왜 C Union이 필요한가요?


236

노동 조합은 언제 사용해야합니까? 왜 필요한가요?

답변:


252

정수는 종종 이진 표현의 정수와 부동 소수점 사이를 변환하는 데 사용됩니다.

union
{
  int i;
  float f;
} u;

// Convert floating-point bits to integer:
u.f = 3.14159f;
printf("As integer: %08x\n", u.i);

이것은 C 표준에 따라 기술적으로 정의되지 않은 동작이지만 (가장 최근에 작성된 필드 만 읽어야 함) 거의 모든 컴파일러에서 잘 정의 된 방식으로 작동합니다.

또한 유니온은 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가 28이 아니라 12 바이트 만됩니다.


University of Florida의 대신이 UY해야한다
아 미트 싱 토 마르

1
float를 정수로 변환한다고 가정하는 예가 작동합니까? int와 float는 메모리에 다른 형식으로 저장되므로 그렇게 생각하지 않습니다. 당신의 예를 설명해 주시겠습니까?
spin_eight

3
@spin_eight : float에서 int로 "변환"되지 않습니다. "float의 이진 표현을 int 인 것처럼 재 해석"과 비슷합니다. 출력은되지 않습니다 3 : ideone.com/MKjwon 아담 16 진수로 인쇄 왜 내가하지만, 확실하지 않다.
endolith

@ 아담 로젠 필드 나는 실제로 변환을 과소 평가하지 않았다 나는 출력에서 ​​정수를 얻지 못한다 : p
The Beast

2
정의되지 않은 동작에 대한 면책 ​​조항을 제거해야한다고 생각합니다. 실제로는 정의 된 동작입니다. C99 표준의 각주 82를 참조하십시오 . 공용체 객체의 내용에 액세스하는 데 사용 된 멤버가 객체에 값을 저장하는 데 사용한 마지막 멤버와 동일하지 않은 경우 값의 객체 표현의 해당 부분은 다음과 같이 해석됩니다. 6.2.6에 기술 된 새로운 유형의 객체 표현 (때때로 "유형 punning"이라고하는 프로세스) 이것은 트랩 표현 일 수 있습니다.
Christian Gibbons

136

유니온은 임베디드 프로그래밍 또는 하드웨어 / 메모리에 직접 액세스해야하는 상황에서 특히 유용합니다. 다음은 간단한 예입니다.

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;

엔디안 (바이트 순서) 및 프로세서 아키텍처는 물론 중요합니다.

또 다른 유용한 기능은 비트 수정 자입니다.

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;

3
위의 @ 아담 로젠 필드 (Adam Rosenfield)의 답변과 함께 여기에 당신의 대답은 완벽한 보완 쌍을 만듭니다 : 당신 은 노조 내에서 구조체를 사용하여 보여주고, 그는 구조체 내에서 노조를 사용하여 보여줍니다 . 임베디드 시스템의 스레드간에 C에서 멋진 메시지 전달 다형성을 구현 하는 구조체 내의 공용 구조체 내의 구조체와 두 가지가 모두 필요하다는 것이 밝혀졌습니다. .
Gabriel Staples

1
나는 틀렸다 : 그것은 구조체 내 노조 내 구조체 내 노조입니다. 내가 가장 내부에서 내림차순으로 쓰는 것처럼 왼쪽으로 작성했습니다. 다른 데이터 형식의 값을 허용하기 위해 가장 안쪽에 다른 공용체를 추가해야했습니다.
가브리엘 스테이플 1

64

저수준 시스템 프로그래밍은 합리적인 예입니다.

IIRC, 유니온을 사용하여 하드웨어 레지스터를 구성 요소 비트로 분해했습니다. 따라서 구성 요소 비트에 8 비트 레지스터에 액세스 할 수 있습니다.

(정확한 구문은 잊었지만 ...)이 구조를 통해 제어 레지스터를 control_byte 또는 개별 비트를 통해 액세스 할 수 있습니다. 주어진 엔디안에 대해 비트가 올바른 레지스터 비트에 매핑되도록하는 것이 중요합니다.

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;

3
이것은 훌륭한 예입니다! 다음은 임베디드 소프트웨어에서이 기술을 사용하는 방법은 다음과 같습니다 edn.com/design/integrated-circuit-design/4394915/...
rzetterberg

34

나는 객체 지향 상속을 대체하는 것으로 두 개의 라이브러리에서 그것을 보았습니다.

예 :

        Connection
     /       |       \
  Network   USB     VirtualConnection

Connection "클래스"가 위 중 하나가되도록하려면 다음과 같이 작성할 수 있습니다.

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#l74


33

통합은 상호 배타적 인 데이터 멤버가 동일한 메모리를 공유 할 수 있도록합니다. 이것은 임베디드 시스템과 같이 메모리가 부족한 경우 매우 중요합니다.

다음 예에서

union {
   int a;
   int b;
   int c;
} myUnion;

이 공용체는 3 개의 개별 int 값이 아닌 단일 int의 공간을 차지합니다. 사용자의 값으로 설정하면 다음의 설정 값 B를 , 그것의 값을 덮어 쓰기 이들이 모두 동일한 메모리 위치를 공유하기 때문이다.


29

많은 사용법. 그냥 grep union /usr/include/*또는 비슷한 디렉토리에 있습니다. 대부분의 경우에 union래핑되어 있고 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;
}

정확히 내가 찾던 것! 일부 줄임표 매개 변수를 대체하는 데 매우 유용합니다. :)
Nicolas Voron

17

다음은 내 자신의 코드베이스 (메모리 및 구문이 정확하지 않을 수 있음)의 통합 예입니다. 내가 작성한 인터프리터에 언어 요소를 저장하는 데 사용되었습니다. 예를 들어, 다음 코드는

set a to b times 7.

다음 언어 요소로 구성됩니다.

  • 상징 [세트]
  • 변수 [a]
  • 기호 [to]
  • 변수 [b]
  • 상징 [시간]
  • 상수 [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;

각 요소의 크기는 최대 합집합의 크기였습니다 (일반적인 값이지만 실제 크기는 구현에 따라 다름).

"set"요소를 작성하려면 다음을 사용하십시오.

tElem e;
e.typ = ELEM_SYM_SET;

"variable [b]"요소를 작성하려면 다음을 사용하십시오.

tElem e;
e.typ = ELEM_VARIABLE;
e.str = strdup ("b");   // make sure you free this later

"constant [7]"요소를 작성하려면 다음을 사용하십시오.

tElem e;
e.typ = ELEM_CONSTANT;
e.val = 7;

float ( float flt) 또는 rationals ( struct ratnl {int num; int denom;}) 및 기타 유형 을 포함하도록 쉽게 확장 할 수 있습니다 .

기본 전제는 점이다 strval그 구조는 메모리 위치에 기초하고, 여기서 도시 된 동일한 메모리 블록에 다른 뷰를 얻는 방법 그래서 그들은 오버랩 실제로 메모리에 인접하지 않은 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상수 요소에서 주석을 제거 해야합니까 ?
Trevor

예, @Trevor, 지난 4 년 동안 처음 본 사람은 믿을 수 없지만 :-) 고정되어 있습니다. 감사합니다.
paxdiablo

7

메모리를 절약하는 것과 같이 다른 방식으로 사용될 수있는 메모리를 더 쉽게 재사용 할 수 있다고 말하고 싶습니다. 예를 들어 짧은 문자열과 숫자를 저장할 수있는 "variant"구조체를 수행하려고합니다.

struct variant {
    int type;
    double number;
    char *string;
};

32 비트 시스템에서는 각 인스턴스에 대해 최소 96 비트 또는 12 바이트가 사용됩니다. variant .

공용체를 사용하면 크기를 64 비트 또는 8 바이트로 줄일 수 있습니다.

struct variant {
    int type;
    union {
        double number;
        char *string;
    } value;
};

더 많은 변수 유형 등을 추가하려면 더 많은 비용을 절약 할 수 있습니다. 공백 포인터를 캐스팅하는 비슷한 작업을 수행 할 수 있다는 것은 사실 일 수 있습니다. 안전한. 이러한 절감 효과는 크지 않지만이 구조체의 모든 인스턴스에 사용되는 메모리의 1/3을 절약합니다.


5

이러한 유형의 유연한 구조가 필요할 때, 다른 크기의 메시지를 보내는 메시지 프로토콜에서 특정 상황을 생각하기는 어렵지만, 더 좋고 프로그래머 친화적 인 대안이있을 수 있습니다.

유니언은 다른 언어의 변형 유형과 비슷합니다. 한 번에 하나만 가질 수 있지만 선언 방법에 따라 int, float 등이 될 수 있습니다.

예를 들면 다음과 같습니다.

typedef union MyUnion MYUNION;
union MyUnion
{
   int MyInt;
   float MyFloat;
};

MyUnion은 가장 최근에 설정 한 것에 따라 int 또는 float 만 포함합니다 . 그래서 이것을하는 :

MYUNION u;
u.MyInt = 10;

u는 이제 10과 같은 정수를 갖습니다.

u.MyFloat = 1.0;

u는 이제 1.0과 같은 부동 소수점을 보유합니다. 더 이상 int를 보유하지 않습니다. printf ( "MyInt = % d", u.MyInt); 특정 동작을 확신하지 못하더라도 오류가 발생했을 수 있습니다.

공용체의 크기는 가장 큰 필드의 크기 (이 경우 float)에 의해 결정됩니다.


1
sizeof(int) == sizeof(float)( == 32) 보통.
Nick T

1
기록을 위해, 다음 INT 인쇄 플로트에 할당하는 것입니다 하지 컴파일러 나 런타임 환경도 같은 오류가 발생 아는 값이 유효합니다. 물론 인쇄되는 int는 대부분의 경우 무의미합니다. 그것은 int로 해석되는 float의 메모리 표현 일뿐입니다.
Jerry B

4

유니온은 하드웨어, 장치 또는 네트워크 프로토콜로 정의 된 구조체를 모델링하거나 많은 수의 객체를 생성하고 공간을 절약하려는 경우에 사용됩니다. 그러나 실제로는 95 %가 필요하지 않으며 디버그하기 쉬운 코드를 고수합니다.


4

이러한 답변 중 다수는 한 유형에서 다른 유형으로의 캐스팅을 처리합니다. 나는 동일한 유형의 조합 (예 : 직렬 데이터 스트림을 구문 분석 할 때)을 최대한 활용합니다. 그것들은 프레임 된 패킷 의 파싱 / 구축 이 사소하게됩니다.

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..
    }
}

편집 엔디안 및 구조체 패딩에 대한 의견은 유효하고 큰 우려입니다. 나는이 코드를 거의 전적으로 임베디드 소프트웨어에서 사용했는데, 대부분 파이프의 양쪽 끝을 제어했다.


1
이 코드는 다음과 같은 이유로 2 개의 다른 플랫폼에서 데이터를 교환 할 경우 작동하지 않습니다 (대부분의 경우). 1) 엔디안이 다를 수 있습니다. 2) 구조물의 패딩.
Mahori

@Ravi 저는 엔디안과 패딩에 대한 우려에 동의합니다. 그러나 나는 이것을 임베디드 프로젝트에서만 독점적으로 사용했다는 것을 알아야합니다. 대부분은 파이프의 양쪽 끝을 제어했습니다.
Adam Lewis

1

노동 조합은 훌륭합니다. 내가 본 유니언의 현명한 사용은 이벤트를 정의 할 때 사용하는 것입니다. 예를 들어, 이벤트가 32 비트라고 결정할 수 있습니다.

이제 32 비트 내에서 이벤트 발신자의 식별자에 대해 처음 8 비트를 지정하고 싶을 수 있습니다. 때로는 이벤트 전체를 처리하고 때로는 분석하여 구성 요소를 비교합니다. 노조는 두 가지 모두를 할 수있는 유연성을 제공합니다.

노조 이벤트
{
  부호없는 long eventCode;
  부호없는 char eventParts [4];
};

1

무엇에 대해 VARIANT그 COM 인터페이스에 사용되는? 여기에는 "type"필드와 "type"필드에 따라 처리되는 실제 값을 보유하는 공용체가 있습니다.


1

학교에서는 다음과 같은 조합을 사용했습니다.

typedef union
{
  unsigned char color[4];
  int       new_color;
}       u_color;

>> 및 << 연산자를 사용하는 대신 색상을보다 쉽게 ​​처리하기 위해 사용했습니다. 문자 배열의 다른 색인을 통과해야했습니다.


1

임베디드 장치를 코딩 할 때 유니온을 사용했습니다. 16 비트 길이의 C int가 있습니다. 그리고 EEPROM에서 읽거나 저장해야 할 때 상위 8 비트와 하위 8 비트를 검색해야합니다. 그래서 나는이 방법을 사용했다 :

union data {
    int data;
    struct {
        unsigned char higher;
        unsigned char lower;
    } parts;
};

코드를 쉽게 읽을 수 있도록 시프트 할 필요가 없습니다.

반면에, 나는 stl 할당 자에 유니온을 사용하는 오래된 C ++ stl 코드를 보았습니다. 관심이 있으시면 sgi stl 소스 코드를 읽으십시오 . 여기 한 조각이 있습니다.

union _Obj {
    union _Obj* _M_free_list_link;
    char _M_client_data[1];    /* The client sees this.        */
};

1
/ struct주위 에 그룹화가 필요하지 않습니까? 지금은 둘 다 첫 번째 바이트 만 가리켜 야합니다. higherlower
마리오

@Mario 아 맞아, 난 그냥 손으로 작성하고 잊어 버려, 감사합니다
Mu Qiao

1
  • 다른 레코드 유형을 포함하는 파일
  • 다른 요청 유형을 포함하는 네트워크 인터페이스.

: 이것 좀보세요 X.25 버퍼 명령 처리를

가능한 많은 X.25 명령 중 하나가 버퍼로 수신되어 가능한 모든 구조의 UNION을 사용하여 제자리에서 처리됩니다.


이 두 가지 예를 모두 설명해 주시겠습니까? 나는 이것이 노조와 어떤 관련이 있는지를 의미합니다
Amit Singh Tomar

1

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];};

컴파일러는 본질적으로 구조체의 크기 (및 정렬) 테이블과 구조체의 멤버 이름, 유형 및 오프셋 테이블을 생성합니다. 컴파일러는 회원이되는 구조에 속한 어느 추적하지 않았고,이 명 구조 유형과이 (멤버로 일치 오프셋 경우에만 동일한 이름을 가진 멤버 가질 수 있도록 할 qstruct xstruct y). p가 임의의 구조 유형에 대한 포인터 인 경우 p-> q는 포인터 p에 "q"의 오프셋을 추가하고 결과 주소에서 "int"를 가져옵니다.

위의 의미를 감안할 때, 함수가 사용하는 모든 필드가 해당 구조 내의 유용한 필드와 정렬되어 있으면 여러 종류의 구조에 대해 유용한 조작을 상호 교환 가능하게 수행 할 수있는 함수를 작성할 수있었습니다. 이것은 유용한 기능이었으며, 해당 구조 유형에 대해 구조 액세스에 사용 된 멤버를 유효성 검증하기 위해 C를 변경하면 동일한 주소에 여러 개의 이름 지정된 필드를 포함 할 수있는 구조가없는 경우에는이를 상실하게됩니다. C에 "union"유형을 추가하면 그 차이를 어느 정도 메울 수있었습니다 (IMHO는 물론 그럴 필요도 없었습니다).

그 차이를 메우는 노조의 능력의 핵심은 노조 멤버에 대한 포인터가 그 멤버를 포함하는 어떤 노조에 대한 포인터로 변환 될 수 있고, 어떤 노조에 대한 포인터도 모든 멤버에 대한 포인터로 변환 될 수 있다는 사실이었습니다. C89 표준은 명시 적 캐스팅 말하지 않았지만 T*직접을 U*모두 포함하는 모든 조합 형식에 대한 포인터로 캐스팅하는 것과했다 TU에 있음을 캐스팅 한 후, 및 U*, 후자의 캐스팅 순서없이 정의 행동에 의해 영향을받을 것 노동 조합 유형이 사용하고 표준에서 직접 주조에 대한 어떤 반대되는 의미를 지정하지 않았습니다 TU. 또한, 기능이 알려지지 않은 기원의 포인터를 통해 객체를 작성하는 동작을 수신 한 경우 T*변환,T*U*을 통해 객체를 읽습니다. U*T 읽는 것은 type 멤버를 통해 공용체를 작성하고 type 으로 읽는 것과 동일합니다 U.이 경우 몇 가지 경우 (예 : 공통 초기 시퀀스 멤버에 액세스하는 경우) 및 구현 정의 (Undefined가 아닌) 나머지). 프로그램이 공용체 유형의 실제 객체로 CIS 보증을 이용하는 것은 드물지만, 원산지가 알려지지 않은 객체에 대한 포인터는 공용체 멤버에 대한 포인터처럼 행동해야하고 관련 행동 보장이 있어야한다는 사실을 악용하는 것이 훨씬 흔했습니다.


`여러 가지 종류의 구조에 대해 유용한 연산을 수행 할 수있는 함수를 작성하는 것이 가능했다. ' 같은 이름의 여러 구조체 멤버를 어떻게 사용할 수 있습니까? 두 구조가 동일한 데이터 정렬을 가지고 있으므로 예와 같은 이름과 동일한 오프셋을 가진 멤버가 있다면 어떤 구조에서 실제 데이터를 생성합니까? (값). 두 구조는 동일한 정렬과 동일한 멤버를 갖지만 그 값은 다릅니다. 자세히 설명해 주시겠습니까
Herdsman

@Herdsman : C의 초기 버전에서 구조체 멤버 이름은 형식과 오프셋을 캡슐화했습니다. 유형과 오프셋이 일치하는 경우에만 다른 구조의 두 멤버가 동일한 이름을 가질 수 있습니다. 구조체 멤버 fooint오프셋이 8 인 경우 anyPointer->foo = 1234;"포인터에서 주소를 가져와 8 바이트로 대체하고 결과 주소에 1234 값의 정수 저장소를 수행합니다. 컴파일러는 anyPointer식별 여부를 알거나 신경 쓸 필요가 없습니다" foo멤버들 사이에 나열된 모든 구조 유형
supercat

포인터를 사용하면 포인터의 '원점'에 관계없이 모든 주소를 역 참조 할 수 있습니다. 그러나 데이터를 가져올 수있는 경우 구조체 멤버 테이블과 이름을 보유하는 컴파일러의 요점은 무엇입니까? 특정 구조의 멤버 주소를 아는 포인터가 있습니까? 그리고 컴파일러가 anyPointer구조체 멤버를 식별 하는지 여부를 알지 못하면 컴파일러는 to have a member with the same name only if the type and offset matched게시물의 이러한 상태를 어떻게 확인 합니까?
목자

@Herdsman : 정확한 행동은 p->foo의 유형과 오프셋에 의존 하기 때문에 컴파일러는 구조 멤버의 이름 목록을 유지합니다 foo. 본질적 p->foo으로의 약어입니다 *(typeOfFoo*)((unsigned char*)p + offsetOfFoo). 후자의 질문에 관해서는, 컴파일러가 구조체 멤버 정의를 만났을 때, 그 이름을 가진 멤버가 존재하지 않거나 그 이름을 가진 멤버가 같은 타입과 오프셋을 가져야합니다. 일치하지 않는 구조체 멤버 정의가 존재하면 스 쿼크했을 것이라고 생각하지만 오류를 처리하는 방법을 모르겠습니다.
supercat

0

간단하고 매우 유용한 예는 ...

상상해보십시오.

당신은이 uint32_t array[2]와 바이트 체인의 3, 4 바이트에 액세스하려는. 당신은 할 수 있습니다 *((uint16_t*) &array[1]). 그러나 이것은 슬프게도 엄격한 앨리어싱 규칙을 위반합니다!

그러나 알려진 컴파일러를 사용하면 다음을 수행 할 수 있습니다.

union un
{
    uint16_t array16[4];
    uint32_t array32[2];
}

기술적으로 이것은 여전히 ​​규칙 위반입니다. 그러나 모든 알려진 표준이이 사용법을 지원합니다.

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