[C++] 데이터 연산 : 산술, 비교, 논리, 비트 연산과 비트 플래그

👻 산술 연산

기본적인 대입 연산이나 사칙 연산(덧셈, 뺄셈, 곱셈, 나눗셈)을 의미한다.


🌱 대입 연산

프로그래밍의 기초라고 볼 수 있는 대입 연산은 등호(=)를 이용하여 나타낼 수 있다.

// a에 b를 대입하고 b를 반환하라
a = b;

💡 TIP
코드가 길어지면 스크롤로 이동하기 힘들어진다. 이때 #pragma region 코드를 사용하면 쉽게 코드를 관리할 수 있다.

#pragma region 산술 연산
    a = b;
#pragma endregion

이렇게 하면 해당 구역을 접고 펼 수 있다.


🌱 사칙 연산

사칙 연산은 말 그대로 덧셈, 뺄셈, 곱셈, 나눗셈을 의미한다. 각각 +, -, *, /, %(나머지) 기호를 사용해서 나타낼 수 있다.

사칙 연산 은 언제 필요할까?
게임 만드는 것을 예로 들었을 때 데미지를 계산 하거나, 체력을 깎는다거나, 반복문에서 카운터를 1 증가시킨다거나 등 아주 다양하게 쓰인다.

// 사칙 연산
a = b + 3;  // 덧셈 add
a = b - 3;  // 뺄셈 sub
a = b * 3;  // 곱셈 mul
a = b / 3;  // 나눗셈 div
a = b % 3;  // 나머지 div

a += 3;     // a = a + 3;
a -= 3;
a *= 3;
a /= 3;
a %= 3;

🪐 증감 연산자

데이터를 1씩 증가시키거나 감소시킬 때 사용하는 연산자이다. ++ 혹은 --를 변수 앞이나 뒤에 붙여 사용한다.

// 증감 연산자
a = a + 1;  // add eax, 1 -> inc eax
a++;
++a;
a--;
--a;

b = a++;    // b = a 먼저 실행된 후 a를 1 증가
b = ++a;    // a를 1 증가시킨 후 b = a

연산은 곱셈, 나눗셈 👉 덧셈, 뺄셈 순으로 진행된다.


🪐 디스어셈블리로 코드 살펴보기

우선 a = b; 코드에 브레이크 포인트를 잡고 디버그 실행 후 디스어셈블리 창을 열어보았다.

Alt Text

a = b;이 한 줄의 코드가 두 줄의 mov 명령어로 진행된 것을 알 수 있다.

밑으로 내리면 나머지 코드들도 확인할 수 있다.

Alt Text

나머지는 edx에서 가져오는 것도 확인 가능하다.

내가 입력한 코드를 컴퓨터가 어떻게 분석하고 데이터를 가공, 처리하는 지에 대해 쉽게 알 수 있을 것 같고, 코드를 줄인 연산이나 증감 연산자의 경우 실제로도 연산 속도가 더 빨라지는 지도 확인이 가능할 것 같다.


👻 비교 연산

서로 다른 두 수를 비교하는 연산이다. 보통 대소비교, 일치/불일치 비교를 의미하며 부등호(>, <)나 등호(=) 등을 사용하여 연산한다.

비교 연산 은 언제 필요할까?
게임 만드는 것을 예로 들었을 때 체력이 0이 되면 사망 하거나, 체력이 30% 이하면 궁극기를 발동시킨다거나, 경험치가 100 이상이면 레벨업한다거나 등 아주 다양하게 쓰인다.

a == b;   // a와 b의 값이 같은가? 👉 같으면 1, 다르면 0을 반환
a != b;   // a와 b의 값이 다른가? 👉 다르면 1, 같으면 0을 반환

// 아래 비교 연산은 모두 참이면 1, 거짓이면 0을 반환
a > b;    // a가 b보다 큰가?
a >= b;   // a가 b보다 크거나 같은가?
a < b;    // a가 b보다 작은가?
a <= b;   // a가 b보다 작거나 같은가?

디스어셈블리로 보면 우리가 배웠던 cmp, jne, jmp등의 명령어를 사용하는 것을 알 수 있다.
Alt Text


👻 논리 연산

최소 두개의 조건을 동시에 확인해야할 때 주로 사용되는 연산이다. NOT, AND, OR 등이 있고, 보통 비교 연산을 하는 조건문 두 가지를 동시에 판별할 때 사용한다. 각각은 !, &&, ||기호를 사용한다.

논리 연산 은 언제 필요할까?
게임 만드는 것을 예로 들었을 때 로그인시 아이디와 비밀번호가 동시에 같아야 한다거나, 길드 해산 시 해당 유저가 길드 마스터거나 운영자 계정이었을 때만 가능하게 해야한다거나 등 아주 다양하게 쓰인다.

// ! (not) : 0이면 1, 그 외는 0을 반환
test = !isSame; // isSame이 false거나 0이면 test는 true를 반환

// && (and) : 두 조건 모두 1이면 1, 그 외는 0을 반환
test = (hp <= 0 && isInvincible == false);

// || (or) : 두 조건 중에 하나만 1이면 1, 모두 0이면 0을 반환
test = (hp > 0 || isInvincible == true);

디스어셈블리로 보면 비교 연산과 마찬가지로 우리가 배웠던 cmp, jne, jmp등의 명령어를 사용하는 것을 알 수 있다.
Alt Text


🌱 AND 논리 연산자 분석해보기

Alt Text
위의 코드는 hp <= 0isInvincible == falseAND 연산과정이다.

다른 비교, 논리 연산과는 다르게 코드가 좀 긴 것을 알 수 있는데, 한 줄씩 차근차근 분석해보자.

// 주소값 앞에 보기 쉽게 줄 번호 추가

01    00465481  cmp         dword ptr [hp (046A048h)],0  
02    00465488  jg          __$EncStackInitStart+235h (04654A1h)  
03    0046548A  movzx       eax,byte ptr [isInvincible (046A04Ch)]  
04    00465491  test        eax,eax  
05    00465493  jne         __$EncStackInitStart+235h (04654A1h)  
06    00465495  mov         dword ptr [ebp-0C4h],1  
07    0046549F  jmp         __$EncStackInitStart+23Fh (04654ABh)  
08    004654A1  mov         dword ptr [ebp-0C4h],0  
09    004654AB  mov         cl,byte ptr [ebp-0C4h]  
10    004654B1  mov         byte ptr [test (046A14Dh)],cl  
  1. hp0 비교 👉 hp <= 0
  2. jg 명령어로, hp가 0보다 크면 해당 주소값(8번째 줄)으로 이동하라는 의미
  3. [2번을 통과하면 실행] isInvincible값을 eax에 복사
  4. [2번을 통과하면 실행] eax의 값이 0인지 아닌지 테스트
    (어셈블리어에서 test도 비교문과 비슷하게 동작한다.)
  5. 테스트 한 값이 0(false)이 아니면 해당 주소값(8번째 줄)으로 이동하라는 의미
  6. [5번을 통과하면 실행] 결과값을 1로 세팅
  7. 9번째 줄로 이동하라는 의미
  8. 결과값을 0으로 세팅
  9. 결과값을 cl로 복사
  10. cl값을 test 변수로 복사

여기서 알 수 있는 것은 2번 줄에서 조건이 부합하지 않으면 바로 8번 줄로 이동하게되고, 결과값을 0으로 세팅한 후에 바로 최종값을 나타내준다는 것이다.
이러한 정보들은 조금이라도 더 좋은, 더 효율적인 코드를 작성할 수 있게 만들어준다.

논리 연산시 첫 번째 조건이 틀리면 바로 그 연산을 종료한다는 건 알고 있었지만 정확한 이유는 알지 못했던 경우, 이렇게 어셈블리 코드를 해석하면 이유를 알 수 있다.


👻 비트 연산

비트 단위로 하는 연산이며, ~, &, |, ^, <<, >> 기호를 사용한다. 다른 연산보단 사용 빈도수는 그렇게 높지 않지만 알아두도록 하자.

비트 연산 은 언제 필요할까?
비트 단위의 조작이 필요할 때 주로 사용된다. 대표적으로 비트 플래그(BitFlag)가 있다.

// ~ : bitwise not
// 단일 숫자의 모든 비트를 대상으로 0과 1을 서로 뒤바꾼다.
0101 1000 👉 1010 0111

// & : bitwise and
// 두 숫자의 모든 비트 쌍을 대상으로 AND 연산을 한다.
0101 1000
1001 0110
---------
0001 0000

// | : bitwise or
// 두 숫자의 모든 비트 쌍을 대상으로 OR 연산을 한다.
0101 1000
1001 0110
---------
1101 1110

// ^ : bitwise xor
// 두 숫자의 모든 비트 쌍을 대상으로 XOR 연산을 한다.
// 다르면 1, 같으면 0을 반환한다.
0101 1000   // xa
1001 0110   // xb
---------
1100 1110   // xa' = xa ^ xb

// 같은 xor 연산을 두 번하면 기존의 값이 반환된다는 특성이 있다.
// 👉 암호학적으로 접근하여 주로 사용된다.
1100 1110   // xa'
1001 0110   // xb
---------
0101 1000   // xa = xa' ^ xb : 초기 xa 값이 그대로 나옴

// << : 왼쪽 시프트(비트 좌측 이동)
// 비트열을 n만큼 왼쪽으로 이동
// 왼쪽에 넘치는 n개의 비트는 사라지고 오른쪽에 새로 생기는 n개의 비트는 0으로 채워진다.
// *2를 할 때 자주 보이는 패턴 (이진수라서)
0101 1000 << 1 👉 1011 0000

// >> : 오른쪽 시프트(비트 우측 이동)
// 비트열을 n만큼 오른쪽으로 이동
// 왼쪽에 새로 생기는 n개의 비트는 0으로 채워지고 오른쪽에 넘치는 n개의 비트는 사라진다.
// ⭐ 단, 부호 비트가 존재할 경우(음수일 경우), 부호 비트는 유지된다.
0101 1000 >> 1 👉 0010 1100
1101 1000 >> 1 👉 1110 1100

🌱 비트 플래그(BitFlag)

각 비트에 의미를 부여하여 고유 아이디를 만들어내는 방식을 비트 플래그(BitFlag)라고 한다. 플래그깃발이라는 의미이며 깃발을 위로 올리면 on, 아래로 내리면 off를 뜻한다. 이를 확장해 비트 플래그에선 on이면 1, off면 0을 의미한다.


💡 간단한 실습 해보기
4개의 비트를 사용하여 무적, 변이, 스턴, 공중부양의 뜻을 가지는 캐릭터의 상태를 나타내보자.

#include <iostream>
using namespace std;

// 부호를 없애야 우측 시프트(>>)를 하더라도 부호비트가 딸려오지 않는다.
// 현재 상태를 의미한다.
unsigned char flag;

int main()
{
    // 무적, 변이, 스턴, 공중부양 상태를 나타내는 4비트의 수로 비트 연산 해보기
    // 0b0000 [무적][변이][스턴][공중부양]

    // 무적 상태로 만든다
    /* 
        // flag = 8;
        위 처럼 비트에 해당하는 값을 직접 입력해도 되지만, 
        4비트가 아닌 64비트라고 생각했을 땐 수가 굉장히 커지게 된다.
        그래서 아래처럼 조립해서 넣는 게 좋다.
    */
    flag = (1 << 3);    // 지금은 3으로 하드코딩 했지만, 나중엔 무적이라는 값을 3으로 세팅하는 방식을 사용한다.

    // 변이 상태를 추가한다 (무적 + 변이)
    // 1이 하나라도 있으면 1이 반환되는 bitwise or 연산을 사용한다.
    flag |= (1 << 2);

    // 무적인지 확인하고 싶으면? (다른 상태는 관심 없음)
    // 이럴 때 bitmask를 사용한다.
    // bitmask : 해당 위치만 1로 둔 값. 여기선 (1 << 3)
    // 현재 상태값(flag)과 bitmask를 bitwise and 연산하면 원하는 상태값을 얻을 수 있다.
    bool invincible = ((flag & (1 << 3)) != 0);

    // 무적이거나 스턴 상태인지 확인하고 싶다면?
    // bitmask = 0b1010
    // bool stunOrInvincible = ((flag & 0b1010) != 0);
    int mask = (1 << 3) | (1 << 1);
    bool stunOrInvincible = ((flag & mask) != 0);
}

대략적인 코드는 위와 같고, 직접적인 수를 입력하기보단 비트 연산을 사용함으로써 가독성을 높일 수 있다.

flag = (1 << 3); 부분에 브레이크 포인트를 걸고 디버그를 실행해서 각각의 코드가 실행될 때마다 flag의 값이 어떻게 변하는지 확인해보자.

Alt Text

우선 디버그를 실행시키면 flag에는 0이 들어가있다. F10을 눌러 프로시저 단위로 디버그를 실행시켜보자.

Alt Text

flag = (1 << 3); 코드가 실행되고, 위의 사진처럼 flag에는 8(0b1000)이라는 값이 들어가게 된다.

Alt Text

변이 상태를 추가하는 flag |= (1 << 2); 코드가 실행되고, flag의 값은 12(0b1100)가 되는 것을 알 수 있다. (bitwise or연산)

💡 BitMask
해당 값의 상태를 알고 싶을 때 연산에 사용될 수 있도록 만들어진 값이다. 알고싶은 상태의 위치값만 1로 표시한 값이라고 생각하면 쉽다. bitwise and 연산을 이용하면 어떤 상태인지 확인이 가능하다.


🪐 디스어셈블리로 코드 살펴보기

flag = (1 << 3); 코드를 살펴보자.

Alt Text

우리는 (1 « 3)이라고 입력했지만, 빌드할 때 자동으로 8이라는 값으로 계산한 후에 복사하는 것을 알 수 있다. 고로 flag = 8;이나 flag = (1 << 3);은 성능적으로 차이가 없다는 것도 알 수 있다. 하지만 8로 입력하는 것보다 위치를 나타내는 비트 연산 형식으로 입력함으로써 가독성을 높일 수 있다.

그 외에 다른 코드들도 자동변환되어 계산된다는 것을 알 수 있다.


👻 글을 마치며

이번 시간엔 데이터를 가지고 할 수 있는 연산 방법에 대해 공부해보았다. 중간중간 디스어셈블리를 통해 이전에 배웠던 어셈블리어를 어떻게 연결시키고, 코드 한 줄 한 줄이 어떻게 동작하는지 쉽게 알 수 있어서 이해가 잘 된 것 같다.
특히 막연하게 여기저기서 전해들어 알고 있었던 사실들(근데 왜 그러는지는 정확하게 몰랐던 사실들:예를 들어 논리 연산에서 첫 번째 조건이 거짓이면 논리 연산을 바로 종료한다는 경우 같은)을 디스어셈블리를 분석해가며 왜 그런지 알 수 있었던 게 매우 흥미로웠다.
어셈블리어로 복습을 하면서 공부를 진행하다보니 수박 겉핥기 식으로 지식을 구경하는 게 아닌 완전히 내 것으로 컴파일 👉 빌드되는 듯한 느낌이 공부 중간중간 확실하게 느껴졌던 것 같다.
기초 하나는 강하게 확실히 잡히는 기분이 들어서 좋다. 😎


소스코드 보러가기


출처
인프런 Rookies님 강의

Categories:

Updated:

Leave a comment