관리 메뉴

HAMA 블로그

C API 와 C++(STL) 호환에 관한 팁 본문

C++ (비공개)

C API 와 C++(STL) 호환에 관한 팁

[하마] 이승현 (wowlsh93@gmail.com) 2019. 3. 24. 13:24



C++만 사용하고 싶어도 태생적 한계 때문에 C API 와의 호환에 대하여 항상 염두해 둬야 하는게 C++ 개발자들의 숙명입니다. 바이트배열로 직렬화 하는 부분에서도 이러한 랑데뷰가 발생하는데 이번 포스트에서 이에 대한 내용을 정리 하려고 합니다. 구체적으로는 int 형을 char* 로 바꾸는 방식에 대해서 살펴 봅니다.

* 직접 메모리를 할당하는 경우에는 메모리 해제하는 부분등에서 오류를 범할 확률이 높아질테고, std::vector , std::string 등을 사용하면 그런 부분은 해결되나 라이브러리에 대한 이해 없이 사용 할 경우 미묘한 버그를 만들어 내서 더욱 찾기가 어렵게 만들 수 도 있습니다. 모든게 좋은 건 세상에 없지요~ 트레이드오프!! 

C  memcpy

unsigned char * arr= (unsigned char*)malloc(8);
uint64_t x = 200321;
memcpy(arr, &x, 8);

- memcpy를 이용하면 간단히(형변환없이) uint64 타입의 숫자를 바이트배열로 직렬화 할 수 있습니다.
- memcpy는 무엇이 매개변수로 들어 오던 void* 로 받아서 바이트배열로 처리 하니까요.
- 동적으로 할당한 메모리를 해제시켜주는 것을 잊지 마세요. 
- unsigned char arr[8]; 이렇게 스택에 만들어주는게 낫겠죠.

unsigned char * arr = (unsigned char*)malloc(100);
uint64_t x = 200321;
memcpy(arr, &x, 8);

uint32_t y = 52;
memcpy(arr + 8 , &y, 4);

- 이렇게 배열을 크게 잡아서 다양한 타입의 직렬화를 구성할 수도 있을 것입니다.

C++  std::vector 

uint64_t x = 200321;
std::vector<uint8_t> arr(8);
std::copy((uint8_t*)&x, (uint8_t*)&x + 8, &arr[0]);

- vector의 시작 포인터는 &arr[0]; 과 같이 추출할 수 있습니다.
- std::copy 를 이용합니다. 성능은 memcpy와 비슷하며 타입에 대해 좀 더 따지게 됩니다.

- 만약 std::copy(&x, &x + 8, &arr[0]); 이렇게 한다면?? 재앙이 일어 날 겁니다. 오류가 생기진 않죠. 따라서 해결하기 어렵게 될 수 있습니다. 여기서 &x + 8 은 8바이트 뒤의 주소가 아니라, 64바이트가 됩니다.
- vector 는 자동으로 메모리 관리를 해주므로 더 많은 메모리가 필요하면 자동으로 추가 됩니다.
- vector 의 재할당 때문에 느려지는 속도가 염려스러우면 reserve 로 미리 많이 잡아 두면 됩니다. 

std::vector<uint8_t> vec(100);

uint64_t  _blockNum1 = 543210;
std::copy((uint8_t*)&_blockNum1, (uint8_t*)&_blockNum1 + 8, &vec[0]);

uint32_t  _blockNum2 = 12;
std::copy((uint8_t*)&_blockNum2, (uint8_t*)&_blockNum2 + 4, &vec[8]);

- 이렇게 배열을 미리 크게 잡아두고 다양한 타입을 직렬화 할 수 도 있습니다.

다시 숫자타입으로 돌리려면 아래와 같이 하면 될 거 같습니다.
방법1)

uint64_t x = *(uint64_t*)(&vec[0]);
std::cout << "result : " <<  std::setprecision(10) <<  x << std::endl;

방법2)

void toUint64(uint8_t * c , size_t n) {
   uint64_t i = (c[7] << 56) | (c[6] << 48) | (c[5] << 40) | (c[4]) << 32 | (c[3]) << 24 | (c[2]) << 16 | (c[1]) << 8 | (c[0]);
   std::cout << "result : " << i  << std::endl;
}


C++ std::string 

int x = 200321;
std::string arr;
arr.append((const char*)&x, sizeof(int));

- std::string 또한 훌륭히 바이트배열을 조작하는데 사용 할 수 있습니다.
- append 에는 바이트포인트와 타입의 길이가 들어 갑니다.

이것은 아래와 같이 다시 int로 되돌릴 수 있을 것입니다.

const char* pointer = arr.c_str();
int y = *(int*)(pointer);

- c_str()은 vector에는 없고 string에만 있는 것으로 바이트 포인트를 리턴해 줍니다.
-  &arr[0]; 해줘도 에러는 안나겠지만 지양해야합니다. vector와 다르게 string은 연속된 주소를 할당하지 않을 수 있습니다.

// 인코딩
std::setprecision(10);
double x = 2012621.12;
std::cout <<  "start : " << x << std::endl;
std::string arr;
arr.clear();
arr.append((const char*)&x, sizeof(double));

// 디코딩 
const char* pointer = arr.c_str();
double y = *(double*)(pointer);
std::cout << "result : " << y << std::endl;

- 이렇게 어떤 타입이든 직렬화를 할 수 있습니다. string, wstring, map 등등


p.s

Effective STL 의 챕터 2는 아래와 같은 항목이 있습니다. 참고 하시구요. 
Chapter 2 vector와 string

항목 13 : 동적으로 할당한 배열보다는 vector와 string이 낫다.
항목 14 : reserve는 필요 없이 메모리가 재할당되는 것을 막아 준다
항목 15 : 잊지 말자! string은 여러 가지 방식으로 구현되어 있다는 사실을...
항목 16 : 기존의 C API에 vector와 string을 넘기는 방법을 알아두자
항목 17 : 쓸데없이 남은 용량은 "바꿔치기(swap) 묘수"를 써서 없애 버리자
항목 18 : vector 보기를 돌같이 하자

Comments