주인장 공지입니다. Lu's…〃 Diary。

반갑습니다. 루사인이라고 합니다.


1. 책 리뷰 & 프로그래밍 일기 블로그입니다.

2. 가끔 성과 글도 올라와요.

3. @Lusain_Kim 에 상주 중.



환영합니다.

[C++] Data Save (2) Lu's…〃 Programing。

 이전 글과 이어지는 내용이다.

 

 데이터를 저장하기 위해서는 정적 데이터 구조로 저장할 필요가 반드시 존재한다. 동적으로 매우 많은 데이터를 저장하기 위해서는 데이터가 얼마나 많은 설명할 있는 정적 데이터가 선행되어야 한다. 정적 데이터는 단순히 4 byte 정수형 하나라도 상관 없다.

 

 예를 들어, 개행 문자가 포함되는 로그 파일을 저장할 때는 문자열을 어디까지 저장하고 읽을지 알기 어렵다. 이런 약속을 하는 것이다: 처음 4byte 무조건 로그의 사이즈이고, 사이즈만큼만 로그 데이터라고 뒤에 다음 4 byte 다시 로그의 사이즈를 저장하는 식으로 데이터를 저장하자고

 물론, 쓰이지 않는 특정 문자로 구분자(separator) 사용해서 구분자가 나올 때까지 계속 읽자고 수는 있다. 하지만 세상일은 모르는 . 로그 시스템이 학부 2학년 기말고사에만 쓰인다면 다행이지만 현업 실무에 쓰인다면 1 밤길에서 뒤통수와 벽돌이 불우한 조우를 있는 원인으로는 충분하다.

 

<로그 시스템의 저장 예시>

 물론 예시는 그렇게 적절한 예시는 아니다. 사실 이런 방식의 데이터 저장 방식은 네트워크 프로그래밍에서 채팅 패킷을 전달할 자주 사용하는 방법 하나이다.

 

 그렇지만 예시를 ( 쓰는지는 몰라도 어쨌든 기술적으로) 이해할 있다면, 조금 깊숙하게 들어갈 있다. 일단 앞서 알아본 데이터 저장 방식에서는 로그데이터와 로그데이터의 사이즈가 연속적으로 붙어 있어, 데이터를 잘못 저장하면 데이터 전체가 소실될 가능성이 존재한다. 그렇다면 로그 데이터의 사이즈를 먼저 저장하고, 저장할 로그 데이터가 시작하는 위치도 같이 기록해주면 어떨까?

 
using offset_t = uint32_t;
 
struct header
{
        
offset_t offset;
        
size_t size;
};

 

 이런 형태의 header 구조체를 만든다. 만약에 추가하고 싶은 정적 데이터가 있으면 추가해도 된다. 우선 데이터는 최대 100개까지 저장된다고 가정하고(이것도 동적이라면 데이터를 위한, header header 필요하다. 그건 직접 실습해보도록 하자) size 0이라면 데이터라고 판단한다고 하자. 이제 여러분은 header 데이터 사이즈 100개가 가장 먼저 저장된다고 가정하고, 뒤로 로그를 저장하기 시작한다.

 


offset_t gOffset = sizeof(header) * 100;

// sample log

char log[] = "it is just sample!";
 ...
header h;

h.offset = gOffset;
h.size = strlen(log);
gOffset += h.size;

 

 

 간단한 예시라 이렇게 저장하지만, 실제로는 로그를 저장하는 시점에서 offset 기록하는 좋다. 계산 실수로 offset 어긋나면 파일은 사용할 없다.

 

 

 

 여기까지 이해했다면 데이터를 모양으로 저장하기 위한 48가지 방법정도는 떠올릴 있겠지만, 가지 도움이 팁들을 적어본다.

 

  1. 헤더 구조를 맞춰서 만들지 않고, 상위버전과의 호환성을 위해 헤더에 더미 데이터를 넣어둔다.

 이러면 버전이 올라가면서 헤더 구조가 바뀌어도 더미로 잡아 놓은 데이터 크기보다 적게 바뀌었으면 헤더의 구조를 변경하지 않고도 대응이 가능하다. 만약 더미가 없거나, 더미보다 많은 데이터를 추가해야 경우에는 어쩔 없이 파일을 전부 다시 저장해야 한다. 1~2MB 정도 되는 작은 데이터면 상관 없지만, 100MB 넘어가도 시간이 눈에 띄게 걸리는 매우 비싼 연산이 것이다. 물론 헤더 뿐만 아니라 데이터 본체에도 더미 데이터를 넣을 수도 있다.

 

  1. 헤더에 버전 정보를 두어서, 하위호환성을 유지한다.

 프로그램을 배포하면 사용자가 이용하는 버전이 모두 동일할 수는 없다. 버전이 하나가 아니라면. 오랜만에 프로그램 업데이트를 했는데 자신의 데이터를 읽는 불상사가 일어나면 된다. 이를 위해 상위 버전의 프로그램은 되도록 하위 버전의 호환을 해주는 좋은데, 헤더에 버전 정보를 넣어두면, 읽을 해당 버전의 헤더로 읽을 있을 것이다. 저장할 때는 최신 버전으로 저장할 수도 있을 것이고. 물론 대대적인 수정으로 데이터를 호환할 없을 경우도 있겠지만, 가능하다면 호환성에 대한 고민은 하는 좋을 것이다.

 

  1. 데이터 하나하나를 읽는 것보다 데이터 전체를 읽는 훨씬 싸다.

  16 byte 데이터 1000 가량(정확한 데이터 수는 헤더에 저장했다고 가정한다) 저장하는 파일에서, 헤더에서 읽은 개수만큼 for 돌려가며 읽는 것보다 데이터 크기만큼의 배열을 만들어서 번에 읽는 매우 싸다. 특히 정적데이터의 경우에는 이렇게 읽으면 파싱할 필요도 없다. 파일 로드가 정말 간단해지는 것이다.


[C++] Data Save Lu's…〃 Programing。

C/C++ 배우면 후반부에 파일 입출력에 대해 배우게 된다. 간단한 입출력 실습도 하는데 그런다고 문자열 데이터 말고는 데이터를 넣어본 적이 거의 없을 것이다. 구분자는 거의 줄바꿈이고.

 

이번 글에서 다룰 파일 입출력 방식은 C++11 std::fstream 사용하지만 개념 자체는 어떤 방식이든 무관하게 사용이 가능하다.

 

 

 

 우선 예전 기억을 돌이켜보자. 우리는 어떻게 파일 입출력을 하였는가?

 

bool Save()
{
        
int save_int = 5;
        
float save_float = 3.123f;
        std::
string save_string = "hello, world!"s;
 
        std::
fstream fs;
        
fs.open("data.txt", std::ios::out);
 
        
if (!fs)
        {
                
// don't open file ...
                
return false;
        }
 
        
// write

        fs << save_int << std::endl;
        
fs << save_float << std::endl;
        
fs << save_string << std::endl;
        
fs.close();
 
        
return true;
}

 


bool Load()
{
        
int load_int;
        
float load_float;
        std::
string load_string;
 
        std::
fstream fs;
        
fs.open("data.txt", std::ios::in);
 
        
if (!fs)
        {
                
// don't open file ...
                
return false;
        }
 
        
// read
        
fs >> load_int;
        
fs >> load_float;
        
fs >> load_string;
 
        
fs.close();
 
        std::
cout << load_int << std::endl;
        std::
cout << load_float << std::endl;
        std::
cout << load_string << std::endl;
 
        
return true;
}

아마 대충 이랬을 것이다. 기억상 학부 강의에서 이걸 나가는 시기가 되면 '<<이나 >>같은 마법의 부호로 파일을 집어넣는구나!' 라고 대충 이해해도 성공적으로 이해한 수준이었다.

이번 글은 std::fstream 설명하는 것이 목적이 아니기 때문에 깊게 설명하지는 않겠다.

 

 


 여하튼, 실제로 이렇게 데이터를 저장하지는 않는다. 이렇게 변수 단위로 읽는 것은 메모리에 올려진 데이터를 읽는 아니라 디스크에 위치한 데이터를 읽는 것이기 때문에 매우 비용이 , 비싼 연산이기 때문이다.

 

 대부분의 경우, 데이터를 저장할 때는 관련된 정보를 모아 구조체로 만든다. 저장할 데이터 역시 한데 모아 구조체로 만든다. 가지 주의할 점은, 동적 할당을 하는 값이 있어서는 된다.

 

 C/C++ 데이터를 형변환하여 읽을 있다. , 어떤 값이든 1 byte 배열, char* 형변환을 있고 대부분의 I/O 작업은 char* 또는 void* 데이터를 저장하는 방법이 구현되어 있다. 필요한 데이터의 크기인데, 동적 할당이 없는 구조체라면 구조체의 크기가 저장할 크기랑 동일하게 된다. 말은 어려우니 코드로 보자.

 

 먼저 저장할 데이터를 모은 SaveData 구조체는 다음과 같다.

 

struct SaveData
{
        
char id[32];
        
char pw[16];
 
        
float exp;
        
int gold;
};

이걸 저장하거나 불러오자.

 

  1. 저장하기
    fs.write(reinterpret_cast<const char*>(&data), sizeof(SaveData));
  2. 불러오기
    fs.read(reinterpret_cast<char*>(&data), sizeof(SaveData));

 

저장할 때에는 데이터가 변하면 되니까 const char* 넘긴다.

 

이렇게 저장하고 불러오면 된다. 고정된 데이터 길이니까 불러올 때에도 구조체 단위로 읽으라고 하면 되니까 처음에 보여준 코드처럼 하나씩 맞출 필요도 없이 매우 편리하다.

 

 

 

 그러면 이제 동적 할당 등으로 길이가 일정하지 않은 데이터의 저장방법을 살펴보자.

 

차이는 없다. 결국 번에 저장할 있을만큼 저장하고 불러오는 요지인데, 이번에는 얼마나 읽어야할지 없다. 어떻게 있을까?

 

  1. 저장하기 전에 4 byte 또는 8 byte 정수형으로 데이터 크기를 미리 저장한다.
    이러면 고정적으로 4 byte 읽고 값만큼 추가로 읽으면 된다.
  2. 파일의 특정 위치, 예를 들면 파일의 처음이나 끝에
    데이터의 크기와 데이터가 저장된 위치(offset) 저장한다.

 

 대개 번째 방법은 좋지 않다고 말한다. 데이터의 처음 위치만 알면 얼만큼 읽어야할지 있고, 값을 통해 데이터를 해킹할 있기 때문이다. 하지만 여기서는 1번으로 설명하겠다. 코드를 보고 감만 잡으면 활용은 무궁무진하게 있을 것이다.

 

char * dynamicString = new char[] { "hello, world! i'm Lusain." };
uint64_t size = std::strlen(dynamicString);
 
fs.write(reinterpret_cast<const char*>(&size), sizeof(uint64_t));
fs.write(dynamicStringsize);

 

명심할 것은, size fs << size;같은 식으로 저장하지 . 이렇게 저장하면 원하는 데이터 크기만큼 저장하는 아니라 그냥 숫자를 저장하기 때문에, 나중에 읽을 문제가 발생할 있다.

 

 2번은 다음에 설명해보도록 하겠다.


[C] do { } while ( false, false ); Lu's…〃 Programing。


얼마 전에 트위터에 적었던 do { } while ( false, false ); 문에 대한 이야기를 여기에 다시 정리한다.


-------------------------------------------------------

 코딩을 하다 보면, 로직 중간에 탈출할 필요가 생긴다. 가장 좋은 방법은 함수를 만들어 로직을 분리하는 방법이지만, 로직에서 변경하는 변수가 한두개가 아니다보면 골치가 아파온다. 이 때 사용하는 기법은 C에서 몇 가지가 있다.

  1. goto

    이 얼마나 깔끔하고 완벽한 방법인가! 중간에 어디로 가야한다? 그러면 goto지!

    ...하지만 아직도 goto를 보면 경기를 일으키며 호환마마보다 무서워하는 사람들이 많은데, 옛날에는 오죽했을까.
    다음 방법들은 어떻게든 goto를 쓰지 않기 위한 처절한 몸부림이다.

  2. switch - break 문

    어차피 반복할 필요 없으면 switch (1) { default: ~~~ } 구문이면 해결된다. 중간에 break; 만 넣으면 된다.

  3. do ~ while 문

    2와 동일하다. 어차피 do 구문은 실행되기 때문에 while ( 0 ) 로 그냥 빠져나오면 된다.


내가 본 코드는 do - while 문을 썼는데, 여하튼 저런 이유로 사용했었다. 그러면 두 번 째 이상한 거. false, false. 얘는 뭘까?

컴파일러 옵션에서 경고 수준(/W) 4에서는 C4127 경고가 발생한다. 이 경고가 뭐냐며는 '조건식이 상수입니다.'

 swich( 1 )이나 while ( false )의 경우에는 이런 경고가 뜬다. 무작정 경고를 disable하는 것은 또 프로그래머의 미학에 어긋나는 법. 옛 고인물들은 답을 찾았다.

 답은 알고 있으니까 우선 "," 쉼표 연산자에 대해 알아보자.

 쉼표 연산자는 쉼표로 이어진 모든 표현식(expression) 중 가장 오른쪽에 있는 식만을 반환한다. 왼쪽부터 처리는 되는데, 실제 사용되는 값은 가장 오른쪽 식의 반환값이다. 그러니까 int a = (1,2,3,4); 에서 a = 4이다. 

 이 쉼표 연산자가 자주 쓰이는 구문이 for문이다. 프로그래밍 시작했을 당시에 이렇게 쓰는 사람 많았을걸.

    int x, y;
    ...
    for (x = 0, y = 0; ... )

 여하튼, 이 연산자는 최종적으로 가장 오른쪽 식을 반환하지만 왼쪽 식에서 어떤 연산을 했는지는 알 수 없다. 아마 이런 이유 때문에 /W4에서 경고가 동작하지 않는 것 같은데, 나도 확실히는 모르겠다. 정확히 알고 계신 분은 댓글로 남겨주세요 ㅎㅎ...





 ps. 이 트윗 스레드를 올리고 나서 수많은 인용 멘션으로 '차라리 goto 쓰고만다' 'goto 모르나? 츄라이츄라이' 같은 멘션들이 마구마구 달렸는데, 저도 goto 좋아하구요, 이렇게 안 짤거구요, 저는 C++ 유저기 때문에 람다라는 매에에에에우 좋은 대체제가 있읍니다.........

 ps. 최신 언어들은 이런 짓 안 해도 되는 클로저를 제공해줍니다. 예를 들어 C++11부터 추가된 람다. [&](){ ... } 안에 모두 담아버리면 됨. AWESOME.



[Direct2D] GDI 객체(HICON, HBITMAP)를 Direct2D Bitmap(ID2D1Bitmap)으로 변환하는 방법 Lu's…〃 Programing。

WIC(Windows Imaging Component) 통해 GDI 객체(HICON, HBITMAP) WICBitmap 객체로 받아올 있다.

 

 우선 HICON 대해서 다뤄보자. HICON 객체를 들고있는 경우는 거의 없을테니 리소스에서 불러오고 해제까지 하는 예제다. 필요한 가지, ID2D1RenderTarget IWICImagingFactory. 이는 Direct2D 초기화 시점에서 만들어둔 객체를 사용하면 된다.

 

// 미리 만들어진 IWICImagingFactory 객체

ComPtr<IWICImagingFactory>wicFactory;

// 미리 만들어진 render target 또는 device context

ComPtr<ID2D1RenderTarget> pd2dRenderTarget;

 

// 최종적으로 저장할 비트맵

ComPtr<ID2D1Bitmap> bmp;

// GDI D2D 변환을 위한 비트맵

ComPtr<IWICBitmapwicBmp;

 

// Load Icon

auto hIcon = LoadIcon(g_hInstMAKEINTRESOURCE(IDI_ICON));

// Create WICBitmp form HICON

wicFactory->CreateBitmapFromHICON(hIcon, &wicBmp);

 

// WICBitmapPIXEL_FORMAT
// RenderTarget PIXEL_FORMAT 호환이 안되므로
// FormatConverter
사용한다.

ComPtr<IWICFormatConverterwicFormatConverter;
wicFactory->CreateFormatConverter(&wicFormatConverter);
wicFormatConverter->Initialize(
          
wicBmp.Get()
        , 
GUID_WICPixelFormat32bppPBGRA
        , 
WICBitmapDitherTypeNone
        , 
nullptr
        , 
0.f
        , 
WICBitmapPaletteTypeMedianCut
);


// FormatConverter
사용하여 D2D1Bitmap 생성한다.

pd2dRenderTarget->CreateBitmapFromWicBitmap(wicFormatConverter.Get(), &bmp);

 

// Destroy HICON
DestroyIcon(hIcon);

 

 

 

그러면 HBITMAP 어떻게 해야할까? , HICON보다는 복잡하다.

 

 

 

HBITMAP hBitmap;

 

라는 HBITMAP 객체가 있다면, HPALETTE 객체를 사용하지 않았을 경우 다음과 같이 사용하면 된다.

 

// 변환한 D2D 객체에서의 알파채널 옵션

WICBitmapAlphaChannelOption opt = WICBitmapUseAlpha;

wicFactory->CreateBitmapFromHBITMAP(hBitmapNULLopt, &wicBmp);

 

 

일반적인 경우라면 HPALETTE 객체를 사용하지는 않을테니 저렇게만 사용하면 것이다.

 



1 2 3 4 5 6 7 8 9 10 다음