F.R.I.D.A.Y.

비트 연산자 : 함수에 인자 넘기기 본문

DEV/C C++

비트 연산자 : 함수에 인자 넘기기

F.R.I.D.A.Y. 2019. 12. 31. 23:50
반응형

https://pixabay.com/photos/code-code-editor-coding-computer-1839406/

 

비트 연산자 : 메모리 크기 줄이기

최근 비트 연산자에 대한 질문을 들어온지라, 오늘은 비트 연산자에 대해 알아봅니다. 더보기 # 들어가기에 앞서.. 비트 연산자는 프로그래밍에 있어 고급 기술이라 분류할 수 있을 것 같습니다. 따라서 이해도..

pang2h.tistory.com

 위 포스트 마지막에 비트 연산자로 함수에 값을 넘기는 이유에 대해 잠깐 살펴봤습니다. 함수에 값을 전달하는 방법은 여러 가지가 있습니다. 단순 값만 넘길 수도 있고, 참조를 넘길 수도 있습니다. 여기에선 '값'을 넘기는 방식이 아니라 값을 어떻게 넘겨야 잘 넘길 수 있을지를 알아봅니다.

 

# 비트 연산자를 선행으로 알고 있어야 합니다.


함수에 값 전달하는 방식

 우리는 일반적으로 인자 하나에 값 하나를 넘기는 1 대 1 방식을 취합니다. 이런 방식을 따르는 이유는 처음 함수를 배울 때 값을 넘기는 방법으로 이렇게 배웠거니와 다른 방법을 알게 되더라도 이 방식이 간편하고 쉽기 때문입니다.

// 주로 함수에 값을 넘기는 방법
FuncA(a, b, c, d, e, f);

 이 방법은 구조가 간단하고 작성 또한 쉽기에 많이 사용됩니다만, 이 방법이 가지는 가장 큰 문제점이 있습니다.

 

'함수 원형이 변경되었을 때 이용된 모든 부분을 수정해야 한다.'

 

 이 문제는 작은 규모의 프로그램에서는 큰 무리 없이 진행할 수 있지만, 큰 규모의 프로그램이라면 버거울 수 있습니다. 큰 규모의 프로그램을 대표적으로 들라하면, Windows OS를 예시로 들 수 있겠습니다.


스타일만 수 십 개

 굳이 C/C++ 언어가 아니라 .Net 기반의 Form을 예시로 설명해보겠습니다. 제가 만들었던 XdobeFix 시리즈의 GUI를 보면, 아래와 같이 구성됩니다.

타이틀바 아이콘 표시 여부 최소화 버튼 표시 여부 최대화 버튼 표시 여부 닫기 버튼 표시 여부
창 테두리 스타일 모든 창 위로(TopMost)속성 타이틀바 표시 여부 Tab키 활성화 여부

 당장에 포스트를 작성하면서 생각나는 속성들을 적었더니 8개가 나왔습니다. 그렇다면 윈도우(창이라 불리는 것)를 생성할 때 어떻게 값을 넘겨주어야 할까요?

 아마도 이런 방식을 가장 먼저 생각해볼 수 있을 것입니다.

CreateWindow(titleIconVisible, minimumBtnVisible, maximumBtnVisible, closeBtnVisible,
    borderStyle, titleBarVisible, tabkeyActivation, ...);

 여기에 여기서 언급하지 않은 속성도 있을 테니 최소한 이 함수의 인자는 8개 이상이라는 결론에 도달합니다. 그러나 이런 방식은 굉장히 비효율적인 코드입니다. 대부분 Boolean 타입으로 값이 True, 아니면 False인 것들입니다. 심지어 실제 윈도를 생성하는 코드를 보면 위와 같이 널브러져 있지 않습니다.

 CreateWindow(LPCTSTR lpClassName, LPCTSTR lpWindowName, DWORD dwStyle, 
    int x, int y, int nWidth, int nHeight, HWND hWndParent, HMENU hMenu, 
    HANDLE hInstance, LPVOID lpParam);
더보기

# CreateWindow 함수 설명

  1. lpClassName: 윈도우 스타일을 시스템에 등록할 때 사용할 이름입니다.
  2. lpWindowName: 타이틀바에 표시될 이름입니다.
  3. dwStyle: 윈도우 스타일 데이터입니다.
  4. x: 윈도우가 표시될 x 좌표입니다.
  5. y: 윈도우가 표시될 y 좌표입니다.
  6. nWidth: 윈도우의 너비값입니다.
  7. nHeight: 윈도우의 높이값입니다.
  8. hWndParent: 부모 윈도우의 핸들입니다.
  9. hMenu: 주 메뉴의 핸들입니다.
  10. hInstance: 운영체제에서 관리하는 프로그램의 값입니다.
  11. lpParam: WM_CREATE 메시지로 넘겨질 데이터입니다.

 우리가 작성했던 인자들은 모두 dwStyle 인자 하나에 들어갑니다. 어떻게 들어갈까요? 이전 포스트를 잘 보셨다면 쉽게 유추 가능합니다. 비트 연산자를 활용해 저 값들은 넣어보겠습니다.


 

코드로 알아보기

 우리가 만든 CreateWindow의 기본 구조는 다음과 같습니다. 여기에서는 8개의 값만 존재한다고 생각하겠습니다.

CreateWindow(titleIconVisible, minimumBtnVisible, maximumBtnVisible, closeBtnVisible,
    borderStyle, topMost, titleBarVisible, tabkeyActivation);

 우리는 이 인자 8개를 하나로 합칠 수 있습니다. 그렇게 구조를 작성하기 위해서는 일종의 약속이 필요합니다.

#define          SW_ICON 0x00000001
#define       SW_MINIMUM 0x00000002
#define       SW_MAXIMUM 0x00000004
#define         SW_CLOSE 0x00000008

#define SW_BORDER_SIMPLE 0x00000010 // 테두리 스타일은 여러개가 있을 수 있으니
#define SW_BORDER_DOUBLE 0x00000030 // 세 개 작성했습니다.
#define SW_BORDER_BOLDER 0x00000070

#define       SW_TOPMOST 0x00000100
#define      SW_TITLEBAR 0x00000200
#define           SW_TAB 0x00000400

 위 값들은 테두리에 관련된 값을 제외하고는 32비트 중 각 하나의 위치만 1인 값입니다. 따라서 우리는 이 값들을 비트/시프트 연산자를 이용하면 int 변수 하나에 넣을 수 있습니다.

void CreateWindow(int dwStyle){

}

 이렇게 원형이 만들어지면 dwStyle의 값을 이리저리 조작해 각 값을 취합니다.

 

void CreateWindow(int dwStyle){
    if(dwStyle & SW_ICON)       ShowTitleIcon();
    if(dwStyle & SW_MINIMUM)    ShowMinimumBox();
    if(dwStyle & SW_MAXIMUM)    ShowMaximumBox();
    if(dwStyle & SW_CLOSE)      ShowCloseBtn();
    unsigned char borderStyle = dwStyle & (SW_BORDER_BOLDER | SW_BORDER_DOUBLE | SW_BORDER_SIMPLE);
    if(borderStyle){
        unsigned char count = 0;
        while(borderStyle){
            count += borderStyle & 0x01;
            borderStyle >>= 1; 
        }
        void (*border_fp)[4] = {BorderDefault, BorderSimple, BorderDouble, BorderBolder};
        border_fp(count);
    }
    if(dwStyle & SW_TOPMOST)    SetTopMost();
    if(dwStyle & SW_TITLEBAR)   ShowTitleBar();
    if(dwStyle & SW_TAB)        SetTab();
}

 이렇게 작성하게 되면 하나의 인자만 받게 되었음에도 불구하고 굉장히 다양한 속성을 설정할 수 있게 됩니다.


비교하기

 비트 연산자를 활용해 작성한 코드부터 보겠습니다.

// Case 1
void CreateWindow(int dwStyle){
    if(dwStyle & SW_ICON)       ShowTitleIcon();
    if(dwStyle & SW_MINIMUM)    ShowMinimumBox();
    if(dwStyle & SW_MAXIMUM)    ShowMaximumBox();
    if(dwStyle & SW_CLOSE)      ShowCloseBtn();
    unsigned char borderStyle = dwStyle & (SW_BORDER_BOLDER | SW_BORDER_DOUBLE | SW_BORDER_SIMPLE);
    if(borderStyle){
        unsigned char count = 0;
        while(borderStyle){
            count += borderStyle & 0x01;
            borderStyle >>= 1; 
        }
        void (*border_fp)[4] = {BorderDefault, BorderSimple, BorderDouble, BorderBolder};
        border_fp(count);
    }
    if(dwStyle & SW_TOPMOST)    SetTopMost();
    if(dwStyle & SW_TITLEBAR)   ShowTitleBar();
    if(dwStyle & SW_TAB)        SetTab();
}

 그리고 이어서 평소 값 하나에 인자 하나를 사용했던 방식입니다.

// Case 2
void CreateWindow(bool titleIconVisible, bool minimumBtnVisible, bool maximumBtnVisible, bool closeBtnVisible,
    unsigned char borderStyle, bool topMost, bool titleBarVisible, bool tabkeyActivation){
        if(titleIconVisible)     ShowTitleIcon();
        if(minimumBtnVisible)    ShowMinimumBox();
        if(maximumBtnVisible)    ShowMaximumBox();
        if(closeBtnVisible)      ShowCloseBtn();
        switch(borderStyle){
            .
            .
            .
        }
        if(topMost)             SetTopMost();
        if(titleBarVisible)     ShowTitleBar();
        if(tabkeyActivation)    SetTab();
}

 구현을 하는 것에 있어서는 두 방식 모두 큰 차이가 없어 보입니다. 오히려 Case 2 방식이 더 알아보기 편한 것 같기도 합니다.

 중요한 것은 사용하는 방법에 있습니다. 이전에 말했던 것과 마찬가지로 Case 1 방식은 2 방식에 비해 사용에 있어 굉장히 편리함을 갖추고 있습니다.

 우리는 최소화 버튼, 닫기 버튼, 타이틀바 표시 속성만 사용한다고 생각해보겠습니다.

// Case 1
CreateWindow(SW_MINIMUM | SW_CLOSE | SW_TITLEBAR);

// Case 2
CreateWindow(false, true, false, true, 0, false, true, false);
//사용할 속성         *            *               *

 일반적으로 사용하는 Case 2의 경우 단 세 개의 속성만 이용하려 하더라도 함수 원형을 맞추어 모든 인자에 값을 넘겨주어야 합니다. 그러나  Case 1 방식은 필요한 속성에 해당하는 값만 비트 OR( | )을 이용해 넘겨주면 됩니다.

 한눈에 보더라도 Case 1의 방식이 훨씬 간편함을 느낄 수 있습니다.


비트 OR을 써야 하는 이유

 이번에 설명한 방식의 중추의 한 부분은 고정된, 한 자리만 1인 상수입니다.  세 가지 기능 최소화 버튼, 닫기 버튼, 타이틀바 표시 속성을 이용할 때는 아래와 같은 값이 인자로 들어간다고 보면 됩니다.

SW_MINIMUM 0x00000002 (2)
SW_CLOSE 0x00000008 (8)
SW_TITLEBAR 0x00000200 (512)

 이 세 값의 합은 522입니다. 그렇다면 더하기 연산자를 활용하면 안 될까요? 안됩니다. 이 경우는 같은자리의 비트가 1이 아닌 수들의 집합으로 이루어져 있었지만 사람들이 모두 하라는 대로 작성하지는 않습니다. 아래 경우를 보겠습니다.

CreateWindow(SW_MINIMUM + SW_CLOSE + SW_TITLEBAR + 
    SW_BORDER_SIMPLE + SW_BORDER_DOUBLE + SW_BORDER_BOLDER + SW_BORDER_BOLDER);

 이 개발자는 테두리의 모든 속성을 이용한 것에 더해 테두리를 '두껍다'보다 더 두껍게 하고 싶었는지 SW_BORDER_BOLDER 속성을 두 번이나 사용했습니다. 이제 값을 확인해보겠습니다.

 

'810'

 

 이라는 값이 나오게 되었습니다. 이 값이 정말 맞게 적용되었는지 코드를 작성해보겠습니다.

// 속성 적용 여부
void check(unsigned int v){
    printf("SW ICON \t: %s\n",       SW_ICON          & v ? "True": "False");
    printf("SW MINIMUM \t: %s\n",    SW_MINIMUM       & v ? "True": "False");
    printf("SW MAXIMUM \t: %s\n",    SW_MAXIMUM       & v ? "True": "False");
    printf("SW CLOSE \t: %s\n",      SW_CLOSE         & v ? "True": "False");
    printf("SW BORDER SIMPLE:%s\n",  SW_BORDER_SIMPLE & v ? "True": "False");
    printf("SW BORDER DOUBLE: %s\n", SW_BORDER_DOUBLE & v ? "True": "False");
    printf("SW BORDER BOLDER: %s\n", SW_BORDER_BOLDER & v ? "True": "False");
    printf("SW TOPMOST \t: %s\n",    SW_TOPMOST       & v ? "True": "False");
    printf("SW TITLEBAR \t: %s\n",   SW_TITLEBAR      & v ? "True": "False");
    printf("SW TAB \t\t: %s\n",      SW_TAB           & v ? "True": "False");
    
}

// 들어온 속성 값과 일치 여부
void check2(unsigned int v){
    printf("SW ICON \t: %s\n",          (SWP_ICON             & v) == SWP_ICON          ? "True": "False");
    printf("SW MINIMUM \t: %s\n",       (SWP_MINIMUM          & v) == SWP_MINIMUM       ? "True": "False");
    printf("SW MAXIMUM \t: %s\n",       (SWP_MAXIMUM          & v) == SWP_MAXIMUM       ? "True": "False");
    printf("SW CLOSE \t: %s\n",         (SWP_CLOSE            & v) == SWP_CLOSE         ? "True": "False");
    printf("SW BORDER SIMPLE: %s\n",    (SWP_BORDER_SIMPLE    & v) == SWP_BORDER_SIMPLE ? "True": "False");
    printf("SW BORDER DOUBLE: %s\n",    (SWP_BORDER_DOUBLE    & v) == SWP_BORDER_DOUBLE ? "True": "False");
    printf("SW BORDER BOLDER: %s\n",    (SWP_BORDER_BOLDER    & v) == SWP_BORDER_BOLDER ? "True": "False");
    printf("SW TOPMOST \t: %s\n",       (SWP_TOPMOST          & v) == SWP_TOPMOST       ? "True": "False");
    printf("SW TITLEBAR \t: %s\n",      (SWP_TITLEBAR         & v) == SWP_TITLEBAR      ? "True": "False");
    printf("SW TAB \t\t: %s\n",         (SWP_TAB              & v) == SWP_TAB           ? "True": "False");
    
}

810 값에 대한 결과(좌: check, 우: check2)

 생각과 달리 810은 BORDER_SIMPLE은 적용되지 않고 TOPMOST 속성이 적용되었습니다. 어째서 이런 일이 벌어졌을까? 답은 간단합니다. 같은 위치 비트를 한 번만 세지 않고 여러 번 세었기 때문입니다.

 SW_BORDER_BOLDER를 두 번 사용했었습니다. 원치 않는 결과를 초래할 수 있기 때문에 산술 연산자인 더하기 연산자를 이용하는 것이 아니라 비트 연산자인 비트 OR( | ) 연산자를 이용하는 것입니다.

 

 참고로 이 속성들을 이용하면 넘겨지는 값은 다음과 같습니다.

 

'634'

좌 check, 우 check2


적절한 사용처

 이 방식은 여러 속성을 하나의 인자에 담아 메모리 절약과 함께, 사용 방법이 간단하다는 장점이 있습니다. 그럼에도 이 방식이 많이 이용되지 않는 이유가 있습니다.

 

 방식 자체가 한정된 공간에 여러 값을 넣게 되다보니 구획을 나누어 넣을 수밖에 없습니다. Boolean 타입, 범위가 굉장히 한정된 값을 함수에 전달할 때 유용하지, 실제 값들(e.g. 산술 연산의 피연산자들)을 넘기는 방법으로는 추천하지 않습니다.

# index

728x90
반응형

'DEV > C C++' 카테고리의 다른 글

typedef  (0) 2020.01.04
구조체(struct) part1. default  (0) 2020.01.02
비트 연산자 : 메모리 크기 줄이기  (0) 2019.12.21
비트 연산자 : 종류  (0) 2019.12.20
void 포인터(메모리)  (0) 2019.12.08
Comments