• 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라는 조건이 걸리는 순간 그 뒤의 조건은 보지도 않고 지나가기 때문이다. 당연하게 여기는 사람들도 있겠지만 이런 부분에서 버그가 발생하여 의도와는 다르게 작동할 수 있다.

  • 3.2.1 sizeof 연산자
  • 3.2.2 쉼표 연산자(Comma operators)
  • 3.2.3 조건 연산자(Conditional operators)

3.2.1 sizeof 연산자

 

sizeof는 그동안 많이 사용했지만 이걸 함수로 생각해야 할지 연산자로 생각해야 할지 고민해볼 시간을 가지진 않았다. C++에서 함수는 마지막에 ()를 사용하여 끝내는데, sizeof는 괄호가 필요 없다. 이에 따라 sizeof를 연산자로 생각하는게 타당하다.

#include <iostream>

int main()
{
	using namespace std;

	float floatNumber;

	cout << sizeof(float)		<< endl;
	cout << sizeof floatNumber	<< endl;

	return 0;
}

sizeof를 사용할 때 가장 많이 사용하는 것은 자료형을 괄호로 감싸서 사용하는 것이나 두 번째 예시를 보면 괄호가 필수적으로 사용되는 것은 아님을 알 수 있다.


3.2.2 쉼표 연산자(Comma operators)

 

쉼표 연산자, 콤마 연산자에 대해서도 크게 생각해보는 시간이 없었는데 이번 기회에 간단하게 정리할 수 있었다. 쉼표 연산자는 모든 연산자들 중에 우선순위가 가장 낮은 연산자이다. 일반적으로 같은 자료형의 메모리 식별자를 선언해줄 때 사용하는데, 개인적으로 모호해질 수 있는 경우가 많아 자주 쓰는 형태는 아니었다.

#include <iostream>

int main()
{
	using namespace std;

	int number01 = 3, number02 = 10;
	int number03 = (++number01, ++number02);

	cout << number01 << " " << number02 << " " << number03 << endl;

	return 0;
}

이런 형태로 코드를 구현하는 경우는 매우 드물겠지만 이처럼 작성하는 경우 number03에 어떤 수가 어떻게 들어갈지 굉장히 모호해진다.


3.2.3 조건 연산자(Conditional operators)

 

삼항 연산자로도 불리는 조건 연산자는 의외로 많이 쓰이는 연산자이다. 간단한 조건에 대한 판단을 할 때 코드가 짧아질 수 있고 가독성 또한 높아진다. if else문으로 다루는 것과 다르지 않지만 어떠한 조건에 의해 여러 줄로 코드를 수행하는 경우가 아니라면 쓸만한 연산자이다.

#include <iostream>

int main()
{
	using namespace std;

	// conditional operator (arithmetic if)
	bool onSale = true;

	int price;

	if (onSale)
		price = 10;
	else
		price = 100;

	cout << price << endl;

	// price를 const로 쓰고싶다면?
	const int constPrice = (onSale == true) ? 10 : 100;

	int bb = 5;
	cout << ((bb % 2 == 0) ? "even" : "odd") << endl;

	return 0;
}

삼항 연산자는 조건이나 반환하는 값이 간단할 때만 사용하자. 삼항 연산자의 반환값은 같은 자료형으로 쓰는 걸 권장한다.

  • 3.1.1 연산자 우선순위(Operator Precedence)
  • 3.1.2 결합 법칙(Associativity) 활용
  • 3.1.3 산술 연산자(arithmetic operators)
  • 3.1.4 증감 연산자(increment decrement operators)

3.1.1 연산자 우선순위(Operator Precedence)

 

사칙연산을 배울 때 우리는 덧셈과 뺄셈보다 곱셈과 나눗셈을 먼저 계산한다는 우선순위를 정해서 연산을 수행한다. 프로그래밍 언어도 많은 연산자가 존재하기 때문에 연산자에 대한 우선순위가 필요하다. 연산자 우선순위를 의도적으로 외우려 하는 것은 매우 힘들기 때문에 일반적으로 연산자 우선순위에 대한 표를 검색으로 찾아서 보거나 결합 법칙을 활용한다.

 

https://en.wikipedia.org/wiki/Operators_in_C_and_C%2B%2B

 

Operators in C and C++ - Wikipedia

From Wikipedia, the free encyclopedia Jump to navigation Jump to search Similar syntax in both computer languages This is a list of operators in the C and C++ programming languages. All the operators listed exist in C++; the column "Included in C", states

en.wikipedia.org


3.1.2 결합 법칙(Associativity) 활용

 

연산자 우선순위는 프로그래밍을 계속하면 몇 가지는 체화되지만 안 되는 것들이 존재할 수 있다. 그럴 때는 의도적으로 괄호를 통해 결합 법칙을 이용하는 방법을 생각할 수 있다. 순서가 애매하다면 프로그래머가 직접 ()를 통해 순서를 확실히 한다.

#include <iostream>

int main()
{
	using namespace std;
	
	int x = 4 + 2 * 3;	// 약속된 연산자 우선순위로 계산된다.
	int y = (4 + 2) * 3;	// 결합법칙 활용

	return 0;
}

경험적으로 봤을 때 결합법칙을 활용하는 경우는 조건문에 많은 조건들이 붙거나 매크로가 여러 번 걸쳐지면서 애매해지는 순간 정도였던 것 같다.


3.1.3 산술 연산자(arithmetic operators)

 

산술 연산자는 우리가 흔히 알고 있는 사칙연산과 나머지 값을 반환하는 modulus operator(나머지 연산)이 있다. 또한 피연산자의 수에 따라 산술 연산자를 분류할 수 있다.

 

아래는 피연산자가 하나인 단항 연산자이다. 우리가 흔히 숫자에 부호를 넣어주는 형태로 사용할 수가 있다.

#include <iostream>

int main()
{
	using namespace std;

	int x = 1;

	int unaryOperatorMinus	= -x;
	int unaryOperatorPlus	= +x;

	return 0;
}

 

아래는 피연산자가 두 개인 이항 연산자 중 나누기와 나머지 연산에 대해 다루었다.

#include <iostream>

int main()
{
	using namespace std;

	int operand1 = 7;
	int operand2 = 4;
	cout << operand1 / operand2			<< endl; // return  1
	cout << float(operand1) / operand2		<< endl; // return  1.75
	cout << operand1 / float(operand2)		<< endl; // return  1.75
	cout << float(operand1) / float(operand2)	<< endl; // return  1.75
	
	cout << -5	/  2	<< endl; // c++ 11부터는 소수점 아래를 절삭하기로 했음 return -2
	cout << -5	%  2	<< endl; // return -1
	cout << -5	% -2	<< endl; // return -1
	cout <<  5	% -2	<< endl; // return  1

	// 복합 연산자
	// 코딩 작업 내용을 줄여준다.
	// 실수를 줄여준다. 보기가 편해진다.
	operand2 += operand1;

	return 0;
}

정수 자료형에 대해 나누기 연산을 수행할 경우 주의할 점은 소수점 아래가 절삭된다는 점이다. 이전에는 이에 대해 어떻게 처리해야 할지 컴파일러마다 모호했지만 C++ 11부터는 소수점 아래를 절삭하는 것으로 결정 났다. 음수에 대한 나머지 연산 또한 return 값에 대해 주의 깊게 볼 필요가 있다. 나누는 피연산자의 부호는 상관없이 나누어지는 수의 부호에 따라 결과 값이 달라진다.

 

이항 연산자는 복합 연산자가 존재한다. 복합 연산자는 코딩 작업 내용을 줄여주며 이에 따라 실수를 줄여줄 수 있고 보기가 편해진다. 처음에는 불편할지 몰라도 계속 쓰다 보면 익숙해지는 것이 복합 연산자이다.


3.1.4 증감 연산자(increment decrement operators)

 

반복문에서 자주 볼 수 있는 연산자가 바로 증감 연산자이다. 반복문 내에서 반복 횟수를 설정하기 위해서 사용한다면 크게 고민 없이 사용할 연산자이지만 그 외의 경우에 쓴다면 전위와 후위에 따른 결과의 차이를 알고 있어야 한다.

#include <iostream>

int main()
{
	using namespace std;

	int number = 5;
	int prefixDecrementNumber	= --number;
	int postfixDecrementNumber	= number--;

	cout << prefixDecrementNumber	<< endl;
	cout << postfixDecrementNumber	<< endl;

	return 0;
}

 

위 코드의 실행 결과는 아래와 같다.

결과

여기서는 차이를 느낄 수 없는 구조이지만 아래의 코드를 실행하면 차이를 확연하게 알 수 있을 것이다.

#include <iostream>

int main()
{
	using namespace std;

	int number01 = 6;
	int number02 = 6;
	cout << number01	<< " " << number02	<< endl;
	cout << ++number01	<< " " << --number02	<< endl;
	cout << number01++	<< " " << number02--	<< endl; // number01을 스트림으로 보낸 뒤에 1을 더해준다.
	cout << number01	<< " " << number02	<< endl;

	return 0;
}

 

전위 후위 연산자 결과

전위 증감 연산자는 해당 연산자가 존재하는 명령문에 도착했을 때 명령문을 수행하기 전 증감 연산자를 수행하고 후위 증감 연산자는 해당 연산자가 존재하는 명령문을 먼저 실행하고서 증감 연산자를 수행한다. 즉, 전위 후위 연산자에 따라 해당 연산자가 존재하는 명령문의 결과가 달라지니 이를 염두에 두고 프로그래밍을 해야 한다.

 

#include <iostream>

int add(int a, int b)
{
	return a + b;
}

int main()
{
	using namespace std;

	int number03 = 1;
	int number04 = 2;
	int addNumber = add(number03, ++number03);

	cout << addNumber << endl;
	addNumber = add(number03, ++number04);
	cout << addNumber << endl;

	return 0;
}

이런 방식으로 프로그래밍을 하면 의도와는 다른 결과를 많이 볼 수 있으니 증감 연산자를 사용할 때는 신중하자.

+ Recent posts