char data[1]의 역할은? by object

C/C++ 퀴즈 하나

물론 고수님들에겐 쉬운 문제지만 다음 구조체에서 char data[1]의 역할이 무엇인지 생각 해봅시다.

typedef struct tagWHATTHE {
int data1;
int data2;
char data[1];
} WHATTHE;

모르시는 분들은 최소 1분 정도 생각을 해봅시다.



그래도 잘 모르겠으면 약간의 힌트:

typedef struct tagWHATTHE {
int size;
int type;
char data[1];
} WHATTHE;



char data[1]의 역할은

난생 처음 이런 구조체를 보면 data[1] 변수가 도대체 어떤 의미인지 감을 잡기 힘들다. 왜 이런 변수가 쓰이는지는 직접 예제 코드를 보는 것이 가장 좋을 것 같다.

typedef struct tagHEADER {
int size;
int type;
char data[1];
} HEADER;

// int data_length, int* data;
HEADER* hd = (HEADER*)malloc(sizeof(HEADER) + data_length);
hd->size = sizeof(HEADER) + data_length;
hd->type = 0;
memcpy(hd->data, data, data_length);
fwrite(hd, 1, hd->size, file_handle);

한 마디로 char data[1]의 의미는 길이가 정해지지 않은 데이터를 담기 위한 일종의 더미 변수라고 보면 된다. 예에서도 있듯이 이런 코딩 테크닉은 어떤 데이터의 헤더를 표현할 때 많이 등장한다. 메모리 덩어리를 할당 받고 그것을 이 헤더 타입으로 캐스팅을 하면 앞 부분은 헤더가 놓이고 바로 뒤에 갖고 싶은 가변 길이 데이터를 쉽게 이을 수 있다. 윈도우 프로그래밍을 해보신 분이라면 아마 이런 것을 자주 봤을 것이다. 예를 들면:

typedef struct _RGNDATAHEADER {
DWORD dwSize;
DWORD iType;
DWORD nCount;
DWORD nRgnSize;
RECT rcBound;
} RGNDATAHEADER;

typedef struct _RGNDATA {
RGNDATAHEADER rdh;
char Buffer[1];
} RGNDATA;

RGN이라는 그래픽에서 그리는 영역을 지정하는 클리핑 객체를 담는 자료구조다. RGNDATA에 채워질 데이터가 몇 개나 올지 알 수 없으므로 이렇게 하였다. 또, 대표적인 비트맵 파일 관련 구조체도 빼놓을 수 없다.

typedef struct tagBITMAPINFO {
BITMAPINFOHEADER bmiHeader;
RGBQUAD bmiColors[1];
} BITMAPINFO;

배열 크기가 여기는 [1]인데 [0]으로 둬도 사실 문제 없다. 그리고 소켓 프로그래밍할 때 보게 되는 hostent에도 이제는 과거 호환성 때문에만 남았지만 크기가 0인 배열을 볼 수 있다. 그런데 보통 크기 0인 배열을 선언하면 컴파일러가 워닝을 띄운다. 그래서 C99 표준에는 flexible array member라고 해서 맨 마지막 구조체 변수의 선언이 char data[] 처럼 될 수 있도록 하기도 한다.

도대체 왜?

그렇다면 왜 이렇게 했을까? 보다 직관적으로 코드를 만든다면 아래처럼 쓸 수도 있다.

typedef struct tagHEADER {
int size;
int type;
char* data;
} HEADER;

HEADER* hd = (HEADER*)malloc(sizeof(HEADER));
hd->size = 1024;
hd->type = 0;
hd->data = (char*)malloc(hd.size);
...

그러나 이 접근은 단점이 있다. 위에서 예를 들었듯이 이 자료구조를 파일에 쓴다고 하면 헤더 부분 따로 그리고 data 영역을 따로 두 번에 걸쳐 파일 쓰기를 해야 한다. 결국 이 말은 이 자료구조에 접근하려면 헤더 한번, 실제 한번, 즉 두 번의 참조가 필요하다는 이야기다.

여기서 쪼잔하게 따지면 위 코드는 데이터 참조의 지역성(locality)이 줄어들어 캐시 미스를 한번 더 유발할 수 있는 단점도 있다. 요즘 프로세서들 캐시가 수 메가 바이트니 이 정도 괜찮다라는 생각을 함부로 하지 말자. 정말 성능이 중요한 프로그램은 이런 사소한 지역성 차이에서 오는 차이를 결코 무시할 수 없다. 비슷한 예로 “구조체의 배열(Array of Structure)” 이냐 “배열의 구조체(Structure of Array)”라는 문제도 있다 (우리말과 영어의 어순이 반대라 이거 이름이 참 헷갈린다). 간단한 예를 보면..

구조체의 배열(Array of Structure, AoS):

struct {
double x;
double y;
double z;
} vector[4];

배열의 구조체(Structure of Array, SoA):

struct {
double x[4];
double y[4];
double z[4];
} vector_set;

각각 상황에 따라 적절히 선택해서 골라야 한다. 사소해 보이지만 경우에 따라 큰 성능 차이가 나타날 수 있다.

AoS 같은 접근은 루프 한 순회에서 모든 원소들이 접근될 때 유리하다. vector[i]을 읽으면 일단 i번 째 x, y, z는 모두 캐시에 올라오기 때문이다. 반면 원소는 3개인데 루프에서 고작 x원소만 접근 된다면? 이 vector 배열이 매우 크다면 손실은 심각하다. 그럴 때는 SoA 구조가 바람직하다. 그 외에도 사소한 영향을 더 생각할 수 있지만 생각보다 글이 길어져서 이쯤에서 그만..

한줄요약: char data[1]; 같은 것이 보여도 쫄지 맙시다.


핑백

  • art.oriented : Linux/Mac Makefile 프로젝트를 Visual Studio로 옮기기 2012-10-05 14:35:53 #

    ... T> class VLA 하나 만들고 연산자 오버로딩 몇 개 하면 선언 부분만 대치하는 것으로 말끔히 해결된다.gcc는 size 0의 배열이 지원된다. 헤더 역할을 하는 자료 구조 표현에 편리하다. MSVC는 이게 안 되는데, char temp[] = {}; 같은 것이 있으면 그냥 '\0' 넣어주면 된다.gcc는 0b0101 같 ... more

덧글

  • daybreaker 2009/03/07 14:13 # 삭제 답글

    옥의 티(?) 발견 : "char data[1]의 역할은"이라고 한 부분의 코드에서 int data_length, int *data가 아니라 int data_length, char *data 아닐까요? (뭐, data_length가 데이터의 원소 개수가 아닌 데이터가 차지하는 실제 바이트 크기라고 하면 괜찮겠지만...)

    그나저나 저도 항상 저 끝을 포인터로 두면서 두 번씩 할당해야 하는 것이 불만이었는데 이렇게 하면 비교적 깔끔하게 해결되는군요. 앞으론 이렇게 써야겠습니다. =3=3
  • object 2009/03/08 15:19 #

    네, 말씀대로 int*, char*에 따라 data_length의 의미가 바뀌겠지요.
  • 최종욱 2009/03/08 13:22 # 답글

    전 C, C++ 프로그래머가 아니라서 char data[1];을 보고 '포인터겠군. 근데 왜 1이 아니지? 1에 다른 의미가 있나?'라는 생각이 자꾸 들어서 혼란에 빠져있다가 내려보고서야, '아, 경고를 피하는 용도였구나' 깨달았습니다. ㅠㅠ 여하튼 재미있는 이야기 감사합니다.
  • 궁금... 2009/03/09 10:03 # 삭제 답글

    좋은 글 감사합니다.
    궁금한게 있는데요...
    char data[1];
    이라고 선언을 하면, 1바이트만큼의 메모리가 할당이 될텐데, malloc 없이 memcpy(hd->data, data, data_length); 표현이 가능할지 모르겠네요.

    즉, 1바이트 영역에 data_length만큼 복사가되어 언젠가는 segmentation fault 나 메모리 꼬임(?) 현상이 발생하지 않을까요?
  • object 2009/03/09 10:27 #

    malloc을 할 때 헤더와 헤더 뒤에 따를 데이터의 크기를 한꺼번에 할당합니다. 말씀대로 char data[1]을 하면 1바이트만큼 헤더가 더 할당이 되겠죠. 실제로는 alignment로 4바이트가 더 해지겠지만 그렇다치더라도 이미 데이터가 들어갈 공간은 충분히 확보했기 때문에 전혀 세그폴트가 날 수는 없습니다.

    data가 지금 char [1]로 선언이 되어있지만 char [2] 부터는 바로 뒤 따르는 데이터가 오게 됩니다. 그런 꼼수를 이용한 것이죠.
  • 몽몽이 2009/03/09 11:39 # 삭제 답글

    오랫만에 들렀습니다.
    음... 언젠가 이 방식이 적법한 것이냐에 대한 논쟁을 봤던 기억이 나네요.
    개인적으론 별로 선호하지 않는데... 민장님은 어떠신가요?
  • object 2009/03/09 12:23 #

    저도 이런 걸 제가 직접 써본적은 없습니다 :) 그런데 코드가 모호해진다는 단점 외에는 장점이 더 많다고 봅니다.
  • 궁금... 2009/03/09 13:25 # 삭제 답글

    malloc(sizeof(HEADER) + data_length); 에서 뒷부분을 놓쳤군요...

    제가 자세히 보지 못한것 같습니다.ㅠㅠ
  • felucca 2009/03/10 03:18 # 삭제 답글

    C syntax가 못나서...
  • kalstein 2009/03/10 09:42 # 삭제 답글

    아...좋은 스킬(트릭? ^^;;) 하나 배워가네요.

    전 직관적(?)인 방법만 알고있었는데...저렇게 해도 깔끔하니 좋겠네요.
  • dawnsea 2009/03/10 09:43 # 삭제 답글

    원시적인 네트웍 프로토콜 래핑 래핑 할때 종종 data[0] 으로 썼는데요..
    안 되는 컴파일러도 종종 만납니다;;;
    예) ARM용 SDT -_-;;



  • purnnamu 2009/03/29 09:08 # 삭제 답글

    코드에 코멘트를 적어 보통 사람이 이해하기 쉽게 만들기만 한다면, 성능을 향상시키는 좋은 방법중의 하나가 되겠네요. 프로그램 할때 항상 캐쉬를 고려해야 한다는 점에는 100% 동의합니다. 이런 방법을 널리 공유해 주셔서 감사하게 생각합니다.
  • 마루나래 2010/08/27 00:05 # 삭제 답글

    구조체에 data[1] 이 쓰인 소스가 있어 뭔가 헤맸는데 여기서 답을 찾아가네요~ 좋은 글 감사합니다
  • damduc 2011/01/27 10:02 # 삭제 답글

    저도 소스 보던 중에 궁금하던 했는데 딱 해결 해주셨네요.. ^^
    감사합니다~~!!
  • sail2 2011/02/11 16:07 # 삭제 답글

    지나던 길에 들릅니다. 약간 착오가 있는 것 같아서 드리는 말씀인데요,
    flexible array는 C 규격에서 크기가 없는 array로 선언하도록 되어있습니다.
    위에서 언급하신것처럼 크기를 [0] 으로 잡는 코드는 예전 규격으로 정립되기 이전에 편법으로 쓰이던 방법이고 컴파일러에서 waring이 나던것도 사실이라 규격으로 인정 된 후 [] 로 잡아서 하도록 되어있습니다.

    크기를 [1]로 잡게되면 크게 문제는 없으나 sizeof (구조체) 했을 때 flexible array의 크기까지 합산되기 때문에 나중에 pointer 연산시 offset을 구할 때 항상 고려를 해줘야하는 불편함이 있습니다.
    리눅스 커널 소스에 보면 위와 같은 방식으로 연산하는 부분이 있습니다. 그때 문제가 되겠지요.
  • sail2 2011/02/11 16:09 # 삭제 답글

    struct A {
    int data1;
    int data2;
    char data[];
    };
    에서 sizeof를 구해보면 크기가 8로 나옵니다. (flexible array는 무시하기 때문)
    그러나
    struct A {
    int data1;
    int data2;
    char data[1];
    };
    을 sizeof 해보면 크기가 12가 나옵니다. (char data[1]의 크기가 추가. 구조체 메모리 할당 메커니즘 때문 4가 증가)

    이런 차이가 있습니다.

    캐쉬에 관련된 내용은 정말 도움이 많이 되었습니다.
    이상 저의 짧은 소견이었습니다. 감사합니다.
  • GS 2011/02/15 22:05 # 삭제 답글

    HEADER* hd = (HEADER*)malloc(sizeof(HEADER));
    hd->size = 1024;
    hd->type = 0;
    hd->data = (char*)malloc(hd.size);

    마지막 줄에 hd.size 는 오타이신가요..?
    하여튼, 저는 왜 이걸 원래 방법 처럼 구현하지 않으신지 궁금합니다.
    그러니까.. char* 방법으로도, 한번의 malloc 으로 똑같이 사용하면 무슨 문제가 생기는지요.
  • 2uropa 2013/03/12 16:32 # 삭제 답글

    좋은 방법이네요. WM_COPYDATA를 이용한 ipc에 가변길이 배열을 보낼때
    길이 정보가 포함된 구조체를 전달하고 shared memory로 데이터를 읽도록 처리했는데
    이런 방법이 WM_COPYDATA에서도 이 방법이 유효한지 당장 시험해 봐야겠네요.
    좋은 정보 감사합니다.
    GS 님 : 2년 전이라 이미 해답을 찾으셨겠지만.. 다른 분을위해^^;;
    구조체 객체를 통체로 직접 파일에 쓸때 char*와 같이 선언된다면 데이터가 아닌
    char*포인터가 가르키는 주소 값이 기록되버립니다. 그래서 2중으로 데이터 쓰기가 일어나는거죠.:)
  • 오곡 2013/08/07 16:07 # 삭제 답글

    잘배우고 갑니다 ^^
    댓글도 많이 배우게 됐습니다~!
  • 보셰즈 2013/10/08 00:37 # 삭제 답글

    굉장히 신선하네요. 결국 동적할당을 length만큼 더 받는 것이 결정적이었네요. 잘 보고 갑니당.
댓글 입력 영역