본문 바로가기

C & C++

C/C++ 기초 - 연산자 ( Operator )

연산자 ( Operator )


 '연산자'는 값을 바꾸기 위해 사용합니다. 크게 3가지 종류로 '단항 연산자', '이항 연산자', '삼항 연산자'로 나눌 수 있습니다.

 '단항 연산자'는 연산의 대상이 되는 '피연산자'가 1개인 경우를 뜻합니다. '이항 연산자'는 '피연산자'가 2개, '삼항 연산자'는 3개입니다.

 


 

부호 연산자


#include <iostream>

using namespace std;

int main(void)
{
	char c = 'A';

	cout << -c << endl;	// 출력 결과 -65
	cout << +c << endl;	// 출력 결과 65
	cout << c << endl;	// 출력 결과 A

	return 0;
}

 '부호 연산자'는 양수나 음수로 바꿀 수 있는 '단항 연산자'입니다. 일반적으로 수학의 개념과 비슷하지만 숫자가 아닌 변수에 사용해서 숫자로 출력되게 할 수 있습니다.

 


 

산술 연산자


#include <iostream>

using namespace std;

int main(void)
{
	int a = 10;
	int b = 23;
	float c = 3.14f;

	printf("a + b = %d\n", a + b);  // 출력 결과 33
	printf("a - b = %d\n", a - b);  // 출력 결과 -13
	printf("a / b = %d\n", a / b);  // 출력 결과 0
	printf("a * b = %d\n", a * b);  // 출력 결과 230
	printf("a %% b = %d\n", a % b); // 출력 결과 10

	// 정수와 실수의 산술 연산 결과
	printf("a / c = %f\n", a / c);  // 출력 결과 3.184713
	printf("a * c = %f", a * c);    // 출력 결과 31.400002

	return 0;
}

 '산술 연산자'는 수학에서의 '사칙연산'에 '나머지 연산(%)'이 추가된 '이항 연산자'입니다. 이름 그대로 나눗셈을 한 결과에서 나머지를 출력하는 연산자입니다.

 프로그래밍에서 산술 연산이 실제 수학과 다른 점은 정수 자료형끼리 나눗셈을 하면 소수점은 버려진다는 점입니다. 이를 해결하기 위해선 '형 변환'이 필요한데, '형 변환'에는 '암시적 형 변환'과 '명시적 형 변환'이 있습니다.

 


암시적 형 변환


 '암시적 형 변환'도 데이터 보존이 보장되는 '숫자 승격'과 보장되지 않는 '숫자 변환'으로 나눌 수 있습니다.

 

숫자 승격

#include <iostream>

using namespace std;

int main(void)
{
	short s = 200;
	int i = s;

	return 0;
}

 '숫자 승격'은 비슷한 유형의 자료형에서 메모리 공간의 크기가 작은 자료형과 큰 자료형 사이의 연산에서 발생합니다. 예를 들어, 2byte 정수 자료형인 short의 값을 4byte 정수 자료형인 int에 대입할 때 '숫자 승격'이 발생합니다.

 

숫자 변환

#include <iostream>

using namespace std;

int main(void)
{
	int i = 200;
	short s = i;
	s = 100;
	float f = 3.145f;

	cout << f + i;

	return 0;
}

 '숫자 변환'은 비슷한 유형의 자료형에서 메모리 공간의 크기가 큰 자료형을 작은 자료형의 값으로 대입하거나 다른 유형의 자료형 사이의 연산에서 발생하게 됩니다. 예를 들면 4byte 정수 자료형인 int와 4byte 실수 자료형 float을 더하면 int형은 자동으로 형 변환되어 실수 덧셈으로 진행됩니다.

 short 변수에 int 변수를 넣는 것뿐만 아니라 정수 상수 '100'을 넣을 때도 숫자 변환이 일어납니다. 정수 상수를 int로 인식하기 때문입니다.

 


명시적 형 변환


#include <iostream>

using namespace std;

int main(void)
{
	float f = 3.14;
	int a = (int)f;

	return 0;
}

 명시적 형 변환은 캐스트 연산자 ( (type) )를 사용하여 내가 의도해서 형 변환을 하는 것임을 명확하게 하는 것입니다.

 

 여기서 캐스트 연산자를 사용했는데, 캐스트 연산자를 사용한다는 것은 강제적으로 내가 원하는 형태로 형 변환을 시키는 것이기 때문에 유용하면서도 위험한 방법입니다. 메모리 구조에 대한 이해가 부족하다면 되도록 캐스트 연산을 쓰는 일이 없게 알맞은 자료형을 사용하는 것이 좋습니다.

 


관계 연산자


#include <iostream>

using namespace std;

int main(void)
{
	bool b;
	b = 10 < 20;
	b = 10 <= 20;
	b = 10 > 20;
	b = 10 >= 20;
	b = 10 == 20;
	b = 10 != 20;

	if (true == b)
		cout << "True입니다.";
	else
		cout << "False입니다.";
}

 '관계 연산자'는 연산자의 좌우에 들어오는 피연산자가 들어오는 '이항 연산자'입니다. 두 피연산자의 관계가 크거나 같거나 작은 지를 확인하고 조건과 일치한다면 'True' 아닐 경우에 'False'를 반환하게 됩니다. 한 가지 헷갈릴 수 있는 부분이 'b = 10'과 'b == 10'은 다르다는 점입니다. 프로그래밍에서 'b = 10'은 '10을 b에 저장하겠다'는 의미이고 'b == 10'은 'b와 10이 같은가?'라고 묻는 것입니다.

 


논리 연산자


#include <iostream>

using namespace std;

int main(void)
{
	true && true;	// 결과 True
	true && false;	// 결과 False
	false && false;	// 결과 False

	true || true;	// 결과 True
	true || false;	// 결과 True
	false || false;	// 결과 False

	!true;			// 결과 False
	!false;			// 결과 True

	int a = 10;
	int b = 20;
	bool c = a < b && a != b; // 결과 True
}

 '논리 연산자'는 논리 값(True, False)을 피연산자로 두며 결과도 논리 값을 반환합니다. '&&'는 AND 연산자, '||'는 OR 연산자( | = shift + \ ), '!'는 NOT 연산자이라고 합니다.

 AND 연산자는 두 피연산자가 모두 True인 경우에만 True를 반환합니다.

 OR 연산자는 두 피연산자중 하나만 True여도 True를 반환합니다.

 NOT 연산자는 피연산자와 반대되는 값을 반환합니다. (True는 False, False는 True)

 

 AND 연산과 OR 연산은 모든 논리 값을 확인하지 않을 수도 있습니다.

	if (false && true)
	{
		
	}

	if (true || false)
	{
		
	}

 예를 들어 위처럼 논리 연산이 구현되어 있다고 합시다. AND 연산에서 앞에 오는 피연산자가 false이므로 나올 수 있는 값은 false 밖에 없습니다. 뒤에 오는 피연산자가 true인지 false인지는 확인하지 않습니다.

 OR 연산도 마찬가지로 앞 피연산자가 true라면 결괏값 역시 무조건 true이기 때문에 뒤에 오는 피연산자를 확인하지 않습니다.

 


증감 연산자


#include <iostream>

using namespace std;

int main(void)
{
	int a = 10;
	int b = 10;

	cout << a++ << endl; // 10 출력
	cout << ++b << endl; // 11 출력

	int c = 10;
	int d = 10;

	cout << ++d + ++d + ++d + ++d << endl; // 56 출력
	cout << c++ + c++ + c++ + c++; // 40 출력
}

 '증감 연산자'는 기본적으로 1의 값을 증가시키거나 감소시키는 '단항 연산자'입니다. '포인터', '연산자 오버 로딩' 등 특정 상황에 따라 증감 수치가 다를 수도 있습니다. 증감 연산자에서 중요한 점은 '전위', '후위'가 존재한다는 점입니다.

 전위는 피연산자의 앞에 사용될 경우를 얘기합니다. 피연산자가 식에 포함되어 있는 경우(ex. a = ++a + 1)라면 피연산자를 먼저 증가시킨 후에 식에 대입하게 됩니다.

 후위는 피연산자의 뒤에 사용된 경우입니다. 식에 포함되어 있다면 식에 대입시킨 후 피연산자를 증가시킵니다.

 

 그런데 여기서 문제가 있습니다. 위의 예시코드에서 전위 증감 연산자가 사용된 변수를 4개 더한 값이 56, 후위 증감 연산자가 사용된 변수를 4개 더한 값이 40이라고 되어있습니다. Visual Studio 환경에서는 저렇게 나옵니다.

  위 스크린샷은 스마트폰에서 예시와 같은 코드를 작성했을 때의 결과입니다. 결과가 다른 게 보이시나요? 이런 일이 생기는 이유는 컴파일러마다 차이가 있기 때문입니다. Visual Studio 같은 경우에 증감 연산자가 전위로 오게 되면 일단 모든 전위의 값을 증가시킨 다음 더하기 때문에 56이 나온 겁니다. 스마트폰 환경에서는 묶음 처리가 되어 계산된다고 보는 게 이해하기가 편할 거 같습니다.

 

 ((( ++i + ++i ) + ++i ) + ++i ) 이처럼 묶여서 계산을 해봅시다. 일단 ( ++i + ++i )를 먼저 처리합니다. 전위 증감 연산자가 2번 있으니 ( 12 + 12 )가 되어 값이 24가 나오게 됩니다. 이 값을 다시 다른 ++i와 더하게 됩니다. ( 24 + ++i )에서 24는 더한 값으로 반환된 것이기 상수이므로 i가 증가하여도 변하지 않습니다. 저 식은 결국 ( 24 + 13 )가 되어 37이라는 값을 반환하게 되고 나머지 ++i와 묶으면 ( 37 + ++i ), 증감 연산을 적용시키면 ( 37 + 14 )가 되어 51이 나오게 되는 것입니다.

 

 이 문제는 C 표준으로 증감 연산의 작업을 언제로 할 것인가?에 대한 게 정해져 있지 않아서 생기는 문제라고 합니다. 이러한 경우에 무슨 문제가 발생할지 알 수 없으므로 안전하게 코드를 짜는 습관을 길러야 합니다. 증감 연산자도 위와 같은 문제가 있음을 인지하고 변수를 중복해서 사용하지 않는 안전한 상황에서 사용하시길 추천드립니다.

 


비트 연산자


#include <iostream>

using namespace std;

int main(void)
{
	int a = 11;
	int b = 31;

	cout << ~a << endl;		// 결과 -12
	cout << (a & b) << endl;	// 결과 11
	cout << (a | b) << endl;	// 결과 31
	cout << (a ^ b) << endl;	// 결과 20
	cout << (a << 3) << endl;	// 결과 88
	cout << (b >> 2);		// 결과 7

	return 0;
}

 '비트 연산자'는 bit단위로 연산하는 연산자입니다. 컴퓨터 구조나 프로그래밍에 대해서 잘 모르는 경우에는 이해하기가 좀 어려울 수 있습니다. 비트 연산자는 '보수 연산자 ( ~ )', '비트 AND 연산자 ( & )', '비트 OR 연산자 ( | )', '비트 XOR 연산자 ( ^ )', '시프트 연산자 ( <<, >> )'로 구성되어 있습니다.

 

보수 연산자

 '보수 연산자'는 비트의 값을 뒤집습니다. 1이면 0으로, 0이면 1로 뒤집게 됩니다.

 

비트 AND 연산자

 '비트 AND 연산자'는 비트 단위로 비교하여 양쪽 다 1인 경우에만 1이 됩니다.

 

비트 OR 연산자

 '비트 OR 연산자'는 비트 단위로 비교하여 어느 쪽이든 1이 들어있다면 1이 됩니다.

 

비트 XOR 연산자

 '비트 XOR 연산자'는 비트 단위로 비교하여 두 숫자가 일치하지 않는 경우에만 1이 됩니다.

 

시프트 연산자

( 91 << 3 )

( 91 >> 2 )

 '시프트 연산자'는 비트를 왼쪽이나 오른쪽으로 밀어주는 연산자입니다. 비트를 밀게되면 해당 비트의 영역을 벗어나게 될 수도 있는데 영역을 벗어난 비트는 버려지게 됩니다.

 


할당 연산자


#include <iostream>

using namespace std;

int main(void)
{
	int a = 10;
	int b;

	b = a;
	b += a;	// b = b + a;
	b -= a;	// b = b - a;
	b *= a; // b = b * a;
	b &= a; // b = b & a;

	return 0;
}

 '할당 연산자'는 '대입 연산자'라고도 많이 불리며 기본적으로 변수에 값을 넣는 역할을 합니다. 값을 넣는 과정에서 자기 자신을 포함하여 값을 변경하는 경우( ex. b = b + a ), 특수한 할당 연산자를 사용해서 자기 자신을 생략할 수 있습니다. ( b += a )

 단항 연산자를 제외한 산술 연산자 및 비트 연산자를 할당 연산자와 함께 사용할 수 있습니다.

 


조건식 연산자


#include <iostream>

using namespace std;

int main(void)
{
	int a = 10;
	int b = 20;
	int c = a < b ? a : b;

	cout << c << endl; // 결과 10

	c = a > b ? a : b;

	cout << c; // 결과 20

	return 0;
}

 '조건식 연산자'는 '삼항 연산자'입니다. 흔히 삼항 연산자라고하면 이 조건식 연산자를 말합니다.

 삼항 연산자의 기호로 물음표 ( ? ) 와 콜론 ( : )이 사용됩니다. 물음표의 왼쪽에 있는 피연산자는 논리 값을 가지고 있어야 하며 이 논리 값이 True인지 False 인지에 따라 True면 콜론의 왼쪽 값을, False면 콜론의 오른쪽 값을 반환하게 됩니다.

 


Sizeof() 연산자


#include <iostream>

using namespace std;

typedef struct TEST
{
	int a;
	int b;
}TEST;

int main(void)
{
	cout << sizeof(TEST) << endl;
	cout << sizeof(int);

	return 0;
}

 'sizeof() 연산자'는 자료형의 크기(Byte 단위)를 반환하는 연산자입니다. 여기서 자료형은 C/C++에서 제공하는 기본 자료형을 포함하여 struct, class와 같이 내가 만든 자료형의 크기도 계산해서 알려줍니다.

 


연산자 우선순위


(출처 : MSDN, https://docs.microsoft.com/ko-kr/cpp/c-language/expressions-and-assignments?view=msvc-160)

 

 모든 연산자에는 우선 순위가 있습니다. 하지만 전부 암기하고 있을 필요는 없고 대략적으로 알고 있다가 필요할 때마다 찾아보시거나 우선 순위가 애매하다고 생각되는 경우에 명확하게 괄호로 묶어서 사용하시면 되겠습니다.