C++ 14부터 비트 마스크를 이진법으로 표현해도 가독성 있게 작성하는 방법이 등장했다.


C++ 11까지의 비트 마스크 표현

 

C++11까지는 비트 마스크를 표현하기 위하여 16진법을 사용하여 표현하는 것이 가장 가독성이 좋았다.

const unsigned int red_mask	= 0xFF0000;
const unsigned int green_mask	= 0x00FF00;
const unsigned int blue_mask	= 0x0000FF;

예시 코드처럼 16진수 두 자리를 통해 rgb 값을 표현해서 비트 마스크로 사용할 수 있겠다.


C++ 14부터 추가된 비트 마스크 표현

 

C++ 14부터는 숫자의 단위를 좀 더 명확하게 끊어주기 위한 기호가 추가되었다. 숫자 자리수를 끊어주기 위한 기호이기 때문에 비트 마스크 이외의 용도로도 활용성이 있다는 것을 알아두자!

const unsigned int blue_mask = 0b1111'1111;

이진법으로 blue에 대한 비트 마스크를 정의했는데, 이진수의 경우 4자리씩 끊어서 보는 게 가독성에 좋다. 키보드에서 : 키 오른쪽으로 한 칸 이동한 위치에 있는 '(홑 따옴표) 기호를 이용하면 숫자 사이 자리를 끊어서 표현할 수 있다. 우리가 보기엔 기호가 숫자 사이에 들어가 있어서 문제가 생길 것 같지만 컴파일러는 해당 기호를 삭제하고서 온전히 숫자만 읽는다. 우리가 엔터 및 띄어쓰기를 통한 가독성을 높이는 행위처럼 컴파일러 입장에서는 숫자 사이에 들어있을 때 아무것도 아닌 기호가 된다.

  • 3.4.1 이진수(Binary numbers)
  • 3.4.2 1의 보수와 2의 보수(Complement)
  • 3.4.3 bitset 표준 헤더(bitset standard header)
  • 3.4.4 비트단위 연산자(Bitwise Operators)

3.4.1 이진수(Binary numbers)

 

개발 관련 지식을 쌓다 보면 반드시 만나는 개념이 바로 이진수이다. 우리가 평소 사용하는 수 체계는 십진수로 이루어져 있지만 컴퓨터는 이진수로 모든 것을 해결한다. 컴퓨터 화면에 보이는 것들은 모두 이진수를 조합해서 보여주는 것이다. 이진수는 모든 자리가 0과 1로만 이루어져 있으며, 모든 자리수는 2의 배수로 나타낼 수 있다.

$$ 337 = 300 + 30 + 7 = 3 \times 10^2 + 3 \times 10^1 + 7 \times 10^0 $$

$$ 0101 1110_{(2)} =  0 \times 2^7 + 1 \times 2^6 + 0 \times 2^5 + 1 \times 2^4 + 1 \times 2^3 + 1 \times 2^2 + 1 \times 2^1 + 0 \times 2^0 $$

 

혹시나 이진수에 대해 다룬적이 없다면 우리 친구 위키피디아를 참고하자. 


3.4.2 1의 보수와 2의 보수(Complement)

 

컴퓨터가 -5를 구하려면 어떻게 구할까? 여기서는 두 가지 방법에 대해 소개할 것이며, 추가적인 내용이 필요한 사람이 있다면 마지막에 참고할 위키피디아를 링크로 남겨두겠다.

 

첫번째로 1의 보수를 사용하는 방법이다.

0000 0101  <- 5를 이진수로 표현
1111 1010  <- 0은 1로 1은 0으로 바꾼다.(1의 보수) 
1111 1011  <- 1을 더한 결과

여기서 1을 더하는 이유는 바로 0과 -0을 구분하지 않기 위함이다. 1의 보수만을 사용한다면 0의 경우 두 개가 존재하게 되며 1을 더했을 때 다시 원래의 수로 돌아오는 효과를 받을 수 있다.

0000 0000  <- 0를 이진수로 표현
1111 1111  <- 0은 1로 1은 0으로 바꾼다.(1의 보수) 
0000 0000  <- 1을 더한 결과(최대 자리를 넘어간 수는 버린다.)

 

두 번째로 2의 보수를 사용하는 방법이다.

1 0000 0000  <- 2의 보수를 사용하기 위한 9자리 이진수(맨 앞 숫자만 1)
0 0000 0101  <- 5에 대한 이진수(위의 숫자에서 이 수를 뺀다.) 
0 1111 1011  <- 뺄셈에 대한 결과

 

1의 보수를 구한 뒤 1을 더하는 연산과 2의 보수를 사용하는 것은 동일한 결과를 얻을 수 있게 된다.

 

추가적으로 더 공부하고 싶다면 2의 보수 위키피디아를 참고하자


3.4.3 bitset 표준 헤더(bitset standard header)

 

이진수를 정수 자료형으로도 다룰 수 있겠지만 메모리에 대한 부담이 크다면 bitset 표준 헤더를 쓰는 것도 하나의 방법이 되겠다. 코딩 문제를 풀거나 이진수 구현에서 어느 정도 서포트를 받고 싶을 때 쓰자! 참고로 컴퓨터는 비트를 다루다 보니 비트단위 연산이 자료형 단위 연산보다 빠르게 계산된다.

#include <iostream>
#include <bitset>

int main()
{
	using namespace std;

	unsigned int a = 1024;
	cout << std::bitset<16>(a) << " " << a << endl;

	unsigned int b = 0b1100;
	unsigned int c = 0b0110;
	cout << b << " " << c << endl;

	return 0;
}

a는 bitset 헤더의 도움을 받아서 작성하였다. 총 16자리로 구성된 이진법이다. 나머지 b와 c는 int 자료형에 이진법으로 담았다. 출력을 한 번 해보자!

 

bitset에 대한 추가적인 정보가 필요하다면 네이버 블로그 링크에 잘 설명되어 있으니 참고해보자!


3.4.4 비트단위 연산자(Bitwise Operators)

 

아래 코드에 주석으로 표현한 것들이 비트단위 연산자의 전부이다.

#include <iostream>
#include <bitset>

int main()
{
	using namespace std;

	// <<	left	shift
	// >>	right	shift
	// ~	not
	// &	and
	// |	or
	// ^	xor

	unsigned int a = 1024;
	cout << std::bitset<16>(a) << " " << a << endl;
	cout << std::bitset<16>(a >> 1) << " " << (a >> 1) << endl;
	cout << std::bitset<16>(a >> 2) << " " << (a >> 2) << endl;
	cout << std::bitset<16>(a >> 3) << " " << (a >> 3) << endl;
	cout << std::bitset<16>(a >> 4) << " " << (a >> 4) << endl;
	cout << std::bitset<16>(~a) << " " << (~a) << endl;
    
	unsigned int b = a << 3;
	cout << std::bitset<16>(b) << " " << b << endl;

	cout << std::bitset<4>(a & b) << endl; // bitwise AND
	cout << std::bitset<4>(a | b) << endl; // bitwise OR
	cout << std::bitset<4>(a ^ b) << endl; // bitwise XOR

	return 0;
}

shift 연산은 방향에 따라 모든 비트를 주어진 숫자만큼 이동시킨다. 최대 자리를 초과한 비트 값은 버리게 된다.

not 연산은 모든 자리를 반전시킨다. 0은 1로 만들고 1은 0으로 만든다. 이전에 다룬 1의 보수와 같은 연산이다.

and 연산은 이항 연산자이며 양 옆의 이진수에 대해서 둘다 1인 비트 단위만 1로 남기고 나머지는 0으로 채운다.

or 연산은 동일한 비트 자리에서 하나라도 1이 있으면 1로 쓰고 둘 다 0이면 0으로 쓴다.

xor 연산은 동일한 비트 자리의 수가 일치하면 0 다르면 1을 주는 연산이다.

  • 3.3.1 관계 연산자(Relational operators)
  • 3.3.2 놓치기 쉬운 잘못된 관계 연산자 사용
  • 3.3.3 논리 연산자(Logical operators)
  • 3.3.4 놓치기 쉬운 잘못된 논리 연산자 사용

3.3.1 관계 연산자(Relational operators)

 

관계 연산자는 어려운 개념이 없다. 대소 비교 및 좌우가 같은지 다른지 등을 비교하는 연산자이다. 어떤 표현이 있는지만 잘 알고 있으면 되겠다! 

#include <iostream>

int main()
{
	using namespace std;

	int x, y;
	cin >> x >> y;
	cout << "Your input values are : " << x << " " << y << endl;

	if (x == y)
		cout << "equal" << endl;

	if (x != y)
		cout << "not equal" << endl;

	if (x > y)
		cout << "x is greater than y" << endl;

	if (x < y)
		cout << "x is less than y" << endl;

	if (x >= y)
		cout << "x is greater than y or equal to y" << endl;

	if (x <= y)
		cout << "x is less than y or equal to y" << endl;

	return 0;
}

3.3.2 놓치기 쉬운 잘못된 관계 연산자 사용

 

이처럼 관계 연산자는 쉽기 때문에 많이 범하는 오류가 있다. 실제로 많은 과학 계산 개발자들이 눈치 채지 못하고 걸려드는 문제이다. 바로 부동소수점수에 대한 비교 연산이다. 부동소수점수에 대한 비교 연산은 정말 애매하다. 굉장히 작은 수에 대한 비교가 이루어져야 하는 경우 컴퓨터에서 어쩔 수 없이 생기는 플로팅 에러가 논리적 연산 결과와는 다른 결과를 낼 수 있기 때문이다. 실제로 디버깅하는 업무를 하다 보면 이 부분에서 생기는 문제들을 정말 많이 접한다. 

#include <iostream>
#include <cmath>

int main()
{
	using namespace std;

	double d1(100 - 99.99);	// 0.001
	double d2(10 - 9.99);	// 0.001

	cout << d1 << " " << d2 << endl;
	cout << d1 - d2 << endl;

	const double epsilon = 1e-10;

	if (std::abs(d1 - d2) < epsilon)
		cout << "Approximately equal" << endl;
	/*if (d1 == d2)
		cout << "equal" << endl;*/
	else
	{
		cout << "not equal" << endl;
		if (d1 > d2)
			cout << "d1 > d2" << endl;
		else // d1 < d2 because d1 != d2
			cout << "d1 < d2" << endl;
	}

	return 0;
}

일부러 잘못된 관계 연산자 사용에 대해서는 주석 처리를 하였다. 우리가 생각하기에는 d1과 d2는 같은 수가 나와야 하지만 플로팅 에러의 영향으로 두 수는 미세하게 차이가 난다. 이러한 미세한 차이가 연산을 거듭할수록 불어나기 때문에 항상 주의해야 한다. 따라서 같은 수일지라도 다르게 연산될 수 있기 때문에 입실론이라는 작은 수를 두고 이 수보다 차이가 작은 두 수에 한해서는 근사적으로 같다는 결과로 같음을 표현하기도 한다.


3.3.3 논리 연산자(Logical operators)

 

논리 연산자 또한 크게 어려운 것은 없지만 추후에 다룰 비트 연산자와 혼동하지 않도록 잘 봐두어야 한다. 논리 연산자의 경우 기호를 쓸 때 두 번 쓴다는 것을 기억해두자! 논리 연산자 자체는 어려울 것이 없으니 아래 코드를 두고 설명은 생략하도록 하겠다!

#include <iostream>

int main()
{
	using namespace std;

	bool x = true;
	bool y = false;

	// logical NOT
	cout << !x << endl;

	// logical AND
	cout << (x && y) << endl;

	// logicla OR
	cout << (x || y) << endl;

	// XOR c/c++에는 없는 연산
	// false	XOR false	= false
	// false	XOR true	= true
	// true		XOR false	= true
	// true		XOR true	= false

	return 0;
}

3.3.4 놓치기 쉬운 잘못된 논리 연산자 사용

 

논리 연산자 또한 잘못된 사용을 범할 수 있다. 조건문에서 조건을 논리 연산자로 이어서 작성하는 경우에서 많이 발생한다.

#include <iostream>

int main()
{
	using namespace std;

	// short circuit evaluation
	int x1 = 1; //2로 바꿔서 하면 if의 조건문을 진입안함
	int y1 = 2;

	// 좌측 조건부터 체크함 하나라도 false가 나오면 그 뒤에 조건은 확인 안함
	if (x1 == 1 && y1++ == 2)
	{
		// do something
	}

	cout << y1 << endl;

	bool v1 = true;
	bool v2 = false;
	bool v3 = false;

	bool r1 = v1 || v2 && v3;
	bool r2 = (v1 || v2) && v3;
	bool r3 = v1 || (v2 && v3);

	cout << r1 << endl;
	cout << r2 << endl;
	cout << r3 << endl;

	return 0;
}

어떠한 프로그램을 설계하던지 계산하는 시간을 줄일 수 있다면 최대한 줄여서 빠르게 작동하게 만드는 것이 일반적이다. 조건문에 대한 판단도 그러한 설계 의도에 따라 불필요한 연산을 하지 않는다. 위의 코드를 복사해서 x1에 대한 값을 바꿔가며 사용해보자. 현재 상태에서는 if문 안으로 진입하게 되면서 y1이 조건문을 통과한 순간 3이 된다. 만약 x1을 바꾼다면 조건문에서 y1++는 수행되지 않고 넘어간다. 불필요한 연산을 피하기 위해서 여러 조건이 and로 연결되어 있는 경우 false라는 조건이 걸리는 순간 그 뒤의 조건은 보지도 않고 지나가기 때문이다. 당연하게 여기는 사람들도 있겠지만 이런 부분에서 버그가 발생하여 의도와는 다르게 작동할 수 있다.

+ Recent posts