[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;
코드에 브레이크 포인트를 잡고 디버그 실행 후 디스어셈블리 창을 열어보았다.
a = b;
이 한 줄의 코드가 두 줄의 mov 명령어로 진행된 것을 알 수 있다.
밑으로 내리면 나머지 코드들도 확인할 수 있다.
나머지는 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등의 명령어를 사용하는 것을 알 수 있다.
👻 논리 연산
최소 두개의 조건을 동시에 확인해야할 때 주로 사용되는 연산이다. 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등의 명령어를 사용하는 것을 알 수 있다.
🌱 AND 논리 연산자 분석해보기
위의 코드는 hp <= 0
과 isInvincible == false
의 AND 연산과정이다.
다른 비교, 논리 연산과는 다르게 코드가 좀 긴 것을 알 수 있는데, 한 줄씩 차근차근 분석해보자.
// 주소값 앞에 보기 쉽게 줄 번호 추가
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
- hp와 0 비교 👉
hp <= 0
- ⭐ jg 명령어로, hp가 0보다 크면 해당 주소값(8번째 줄)으로 이동하라는 의미
- [2번을 통과하면 실행] isInvincible값을 eax에 복사
- [2번을 통과하면 실행] eax의 값이 0인지 아닌지 테스트
(어셈블리어에서test
도 비교문과 비슷하게 동작한다.)- 테스트 한 값이 0(false)이 아니면 해당 주소값(8번째 줄)으로 이동하라는 의미
- [5번을 통과하면 실행] 결과값을 1로 세팅
- 9번째 줄로 이동하라는 의미
- 결과값을 0으로 세팅
- 결과값을 cl로 복사
- 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
의 값이 어떻게 변하는지 확인해보자.
우선 디버그를 실행시키면 flag
에는 0이 들어가있다. F10을 눌러 프로시저 단위로 디버그를 실행시켜보자.
flag = (1 << 3);
코드가 실행되고, 위의 사진처럼 flag
에는 8(0b1000)이라는 값이 들어가게 된다.
변이 상태를 추가하는 flag |= (1 << 2);
코드가 실행되고, flag
의 값은 12(0b1100)가 되는 것을 알 수 있다. (bitwise or연산)
💡 BitMask
해당 값의 상태를 알고 싶을 때 연산에 사용될 수 있도록 만들어진 값이다. 알고싶은 상태의 위치값만 1로 표시한 값이라고 생각하면 쉽다. bitwise and 연산을 이용하면 어떤 상태인지 확인이 가능하다.
🪐 디스어셈블리로 코드 살펴보기
flag = (1 << 3);
코드를 살펴보자.
우리는 (1 « 3)이라고 입력했지만, 빌드할 때 자동으로 8이라는 값으로 계산한 후에 복사하는 것을 알 수 있다. 고로 flag = 8;
이나 flag = (1 << 3);
은 성능적으로 차이가 없다는 것도 알 수 있다. 하지만 8로 입력하는 것보다 위치를 나타내는 비트 연산 형식으로 입력함으로써 가독성을 높일 수 있다.
그 외에 다른 코드들도 자동변환되어 계산된다는 것을 알 수 있다.
👻 글을 마치며
이번 시간엔 데이터를 가지고 할 수 있는 연산 방법에 대해 공부해보았다. 중간중간 디스어셈블리를 통해 이전에 배웠던 어셈블리어를 어떻게 연결시키고, 코드 한 줄 한 줄이 어떻게 동작하는지 쉽게 알 수 있어서 이해가 잘 된 것 같다.
특히 막연하게 여기저기서 전해들어 알고 있었던 사실들(근데 왜 그러는지는 정확하게 몰랐던 사실들:예를 들어 논리 연산에서 첫 번째 조건이 거짓이면 논리 연산을 바로 종료한다는 경우 같은)을 디스어셈블리를 분석해가며 왜 그런지 알 수 있었던 게 매우 흥미로웠다.
어셈블리어로 복습을 하면서 공부를 진행하다보니 수박 겉핥기 식으로 지식을 구경하는 게 아닌 완전히 내 것으로 컴파일 👉 빌드되는 듯한 느낌이 공부 중간중간 확실하게 느껴졌던 것 같다.
기초 하나는 강하게 확실히 잡히는 기분이 들어서 좋다. 😎
Leave a comment