• 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;
}

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

  • 2.7.1 Literal Constants
  • 2.7.2 Symbolic Constants
  • 2.7.3 매크로와 상수의 차이점
  • 2.7.4 상수를 사용하여 퇴근을 빨리 하는 방법
  • 2.7.5 const 키워드가 매개변수에 있는 경우

2.7.1 Literal Constants

 

상수는 변하지 않는 수를 의미하며 그중 하나가 리터럴 상수이다. 리터럴 상수에 대한 개념을 처음에는 받아들이기 힘들 수 있는데, 변수처럼 메모리에 이름이 지어져서 할당되는 것이 아니며, 메모리 공간을 가리키는 이름을 가지지 않은 수라 생각하면 좋을 것 같다.

 

일반적으로 우리가 변수에 값을 대입할 때 사용하는 숫자 및 문자가 리터럴 상수가 되겠다. C++ 14에서 Binary Literals도 추가가 되었다.

#include <iostream>

int main()
{
	using namespace std;

	// 숫자나 글자로 표기되는 r-value = literals
	float pi = 3.14f;

	unsigned int n = 5u;
	long n2 = 5L;

	// Decimal	: 0 1 2 3 4 5 6 7 8 9 10
	// Octal	: 0 1 2 3 4 5 6 7 10
	// Hexa		: 0 1 2 3 4 5 6 7 8 9 A B C D E F 10

	int x = 0xF;
	cout << x << endl;
	x = 012;
	cout << x << endl;
	x = 0b1010;
	cout << x << endl;
	x = 0b1011'1111'1010; // 컴파일러는 '를 무시한다. 10진수에도 사용가능하다.
	cout << x << endl;

	return 0;
}

위의 코드에서는 대입 연산자(=) 오른쪽에 있는 것들이 모두 리터럴 상수이다. 바이너리 리터럴 상수를 보면 숫자들 사이에 문자가 들어간 것을 볼 수가 있는데 바이너리 리티럴 상수를 4bit 단위로 끊어서 볼 수 있도록 생겨난 표기법이다. 컴파일러는 '를 무시하므로 써도되고 안 써도 된다. 십진수에서도 사용할 수 있다.

 

숫자 앞에 여러 표현을 활용하면 프로그래밍에서 자주 쓰이는 진수 표현을 쓸 수 있다.

0x : 16진수
0  :  8진수
0b : 2진수

2.7.2 Symbolic Constants

 

심볼릭 상수란 const 키워드와 constexpr 키워드를 통해서 명시적으로 데이터를 상수화시키는 것이다. 심볼릭 상수를 만들면 추후에 해당 상수의 값을 변경하려 할 때 컴파일러가 막아준다. 즉, 우리가 실수할 수도 있는 부분을 컴파일러가 서포트해주는 것이다. constexpr은 C++ 11에서 생긴 것으로 완벽하게 컴파일타임에서 상수로 결정 난다는 것을 키워드로 표현한다.

#include <iostream>

using namespace std;

int main()
{
	const double gravity = 9.8;
	//double const gravity = 9.8; // 이처럼 선언해도 가능하다.

	int number;
	cin >> number;
	const int special_number(number); // 사용자가 입력하는 심볼릭 상수

	constexpr int price_per_item = 123;
    
    return 0;
}

컴파일타임에서 상수임이 결정 나는 것을 컴파일타임 상수라고 칭하며, 런타임에서 결정 나는 경우 런타임 상수라고 말한다. 런타임 상수의 경우 사용자가 입력하는 심볼릭 상수가 된다. 심볼릭 상수는 상수이기 때문에 선언과 동시에 초기화를 해야 한다.


2.7.3 매크로와 상수의 차이점

 

C언어를 먼저 배우는 사람의 경우 매크로를 상수처럼 취급하여 쓰는 경우가 많다. 매크로를 활용하면 컴파일 단계에서 매크로로 만든 리터럴 상수를 치환하여 입력해주기 때문에 편하다. 하지만 매크로를 C++에서는 잘 안 쓰게 되는데 그 이유는 바로 적용 범위가 너무 넓기 때문이다. 매크로가 존재하는 파일을 include하는 경우 그 파일에도 해당 매크로가 적용되기 때문에 객체 지향 언어로 넘어간 C++에서 보는 매크로가 전역 변수처럼 작동하는 게 좋지 않다고 보는 것이다.

#include <iostream>
#define PRICE_PER_ITEM 30 // c에서 많이 쓰지 c++에서는 잘 안쓴다. why? 적용범위가 너무 넓다.

using namespace std;

int main()
{
	constexpr int price_per_item = 123; // 매크로보다 const가 더 좋다.

	return 0;
}

2.7.4 상수를 사용하여 퇴근을 빨리 하는 방법

 

과학 계산을 수행하는 곳에서 업무를 수행하다 보면 많은 상수를 사용하게 되는데, 이를 한 곳에서 모아서 심볼릭 상수로 잡아두면 관리가 편해진다.

 

상수를 모아둔 헤더 파일

 

위처럼 상수를 한 곳에 모아서 두고 상수를 써야 하는 곳에서 include를 해준다.

#include <iostream>
#include "MY_CONSTANTS.h"

using namespace std;

int main()
{
	double radius;
	cin >> radius;
	double circumference = 2.0 * radius * constants::pi;

	return 0;
}

헤더에서 사용한 namespace와 함께 파이를 사용한 코드이다.


2.7.5 const 키워드가 매개변수에 있는 경우

 

함수가 선언된 코드들을 보면 간혹 매개변수에 const 키워드가 붙어 있는 경우를 볼 수가 있다. 매개변수에 const 키워드가 붙어 있는 이유는 해당 함수에서 인자로 들어온 매개변수를 함수 내에서 변경하면 안 된다는 제약 조건을 컴파일 수준에서 정하기 위함이다.

#include <iostream>

using namespace std;

void printNumber(const int my_number)
{
	cout << my_number << endl;
}

int main()
{
	printNumber(123);

	return 0;
}

이처럼 코드를 작성하면 printNumber 함수 안에서 my_number를 수정하게 될 때 컴파일 에러가 뜨게 된다.

+ Recent posts