C++/카루의 C++ 강좌

[C++ 강좌] 2-2. 연산자 I

카루-R 2020. 3. 21. 15:31
반응형

오늘은 변수를 가지고 값을 다루기 위한, 연산자에 대해 알아보겠습니다.

연산자는 크게 산술 연산자, 대입연산자, 비트 연산자, 단항 연산자, 논리 연산자 등이 있습니다.

 

산술 연산자

이건 익숙하실 겁니다.

#include <iostream>

int main()
{
    std::cout << "35 + 46 = " << 35 + 46 << std::endl;
    std::cout << "68 - 27 = " << 68 - 27 << std::endl;
    std::cout << "37 * 19 = " << 37 * 19 << std::endl;
    std::cout << "86 / 24 = " << 86 / 24 << std::endl;
    std::cout << "38 % 11 = " << 38 % 11 << std::endl;
    return 0;
}
35 + 46 = 81
68 - 27 = 41
37 * 19 = 703
86 / 24 = 3
38 % 11 = 5

직관적으로 아실 수 있을 겁니다. 산술 연산자는 더하기, 빼기, 곱하기, 나누기를 하는 연산자입니다.

각각 +, -, *, / 가 그 역할을 담당하며, %는 나머지를 구하는 연산자입니다.

std::cout << "3846 나누기 167" << std::endl;
std::cout << "몫: " << 3846 / 167 << ", 나머지: " << 3846 % 167 << std::endl;
3846 나누기 167
몫: 23, 나머지: 5

정수형 변수 또는 상수를 이용하면 / 로 몫을 구하고, %로 나머지를 구할 수 있습니다.

std::cout << "384.6 나누기 25.9" << std::endl;
std::cout << "몫: " << 384.6 / 25.9 << std::endl;
384.6 나누기 25.9
몫: 14.8494

실수는 이렇게 / 연산자를 사용하면 소수점까지 계산합니다. 단, 출력시에는 자릿수가 잘려서 나옵니다. 이 부분은 C++ 심화 강좌에 출력 형식 조정이 있으니 그 부분을 참고하시면 됩니다.

참고로, % 연산자는 정수만 사용 가능합니다. 실수에 사용하면 오류가 납니다.

참고로 수학에서처럼 * / %(곱셈과 나눗셈) 연산자가 먼저 계산이 되고, + - (덧셈과 뺄셈)이 차례로 계산됩니다.

int n{5};           // n = 5
n = n + 4;          // n = 5 + 4 = 9
n = n * 37;         // n = 9 * 37 = 333
n = n - 90;         // n = 333 - 90 = 243
n = n / 3;          // n = 243 / 3 = 81

std::cout << "n=" << n << std::endl;   // 출력 결과: n=81

변수를 선언하고 나서, 산술 연산을 한 뒤에 그 결과값을 다시 변수에 저장할 수 있습니다. 여기서 =를 대입연산자라고 부르는데, 오른쪽에 있는 값을 왼쪽에 대입하는 역할을 합니다. 이때 대입연산자의 왼쪽에는 반드시 변수가 와야 하며, 오른쪽에 있는 값을 모두 계산한 뒤에 대입을 합니다.

참고로 n + 3이라는 연산을 했다고 n의 값이 바뀌는 게 아닙니다. 이는 단지 n과 3을 더한 값을 나타내는 것 뿐입니다. n에 3을 더하려면 n과 3을 더한 값을 n에 다시 대입해야 합니다. 따라서 최종적으로 이렇게 나타낼 수 있습니다.

n = n + 3;    // n과 3을 더해서, 그 결과값을 다시 n에 저장.

자, 그런데 n에 바로 3을 더하고 싶을 때도 있습니다. n을 두 번씩 쓰기가 귀찮을 때가 있죠.

그래서 산술 연산자는 대입 연산자와 한 번에 쓸 수 있는, 축약형을 제공합니다.

n += 3;      // n에 바로 3을 더함.

이렇게 하면 n에 있는 값이 바로 변경이 됩니다. 등호가 들어있기 때문이죠. 이러한 연산자를 복합 대입 연산자라고 부르는데, 위 예제를 복합 대입 연산자를 사용하면 아래와 같이 됩니다.

int n{5};
n += 4;          // n에 4를 더함, n = 9
n *= 37;         // n에 37을 곱함, n = 333
n -= 90;         // n에서 90을 뺌, n = 243
n /= 3;          // n을 3으로 나눔, n = 81

정수인 변수에 한해서 %= 연산자도 쓸 수 있습니다. 나머지를 대입한다는 뜻입니다.

 

관계 연산자 (비교 연산자)

관계 연산자는 식, 또는 상수/ 변수의 값을 비교할 때 사용합니다. 수학에서의 등호, 부등호의 역할과 비슷합니다.

==, !=, <, <=, >, >= 연산자가 있으며, 각각 =, , <, , >, 과 의미가 비슷합니다. 이들도 연산자 이기 때문에 무언가 값을 반환합니다. 이 값은 무엇일까요?

constexpr auto is_true{1 == 1};

여기서 is_true는 bool 타입 변수로 고정이 되고, 값은 true로 고정이 됩니다.

즉, 관계 연산자는 bool 타입의 true, false를 반환합니다.

#include <iostream>
#include <iomanip>

int main()
{
    std::cout << std::boolalpha;

    std::cout << "7 < 3 : " << (7 < 3) << std::endl;
    std::cout << "7 <= 3 : " << (7 <= 3) << std::endl;
    std::cout << "7 > 3 : " << (7 > 3) << std::endl;
    std::cout << "7 >= 3 : " << (7 >= 3) << std::endl;
    std::cout << "7 == 3 : " << (7 == 3) << std::endl;
    std::cout << "7 != 3 : " << (7 != 3) << std::endl;
    return 0;
}
7 < 3 : false
7 <= 3 : false
7 > 3 : true
7 >= 3 : true
7 == 3 : false
7 != 3 : true

bool 타입 변수와 상수는 그냥 출력하면 0, 1로 출력이 됩니다. 가독성을 위해 true와 false로 출력이 되도록 하려면 위처럼 <iomanip> 헤더 파일을 include하고

std::cout << std::boolalpha;

위 코드를 쓰면 됩니다. bool 타입 변수를 true와 false로 출력한다는 뜻입니다.

참고로, 관계 연산자는 std::cout에 사용할 때 괄호를 꼭 사용해야 합니다. 그 이유는 밑에서 말씀드릴게요. 연산자 우선순위 때문에 그렇습니다.

이러한 관계 연산자는 bool타입 변수에 저장해서 사용하거나, 3단원에서 배울 for, if문 등에서 사용합니다.

if (year == 2020)
{
    std::cout << "올해는 2020년 입니다" << std::endl;
}

관계 연산자는 당연하지만 변수에도 사용할 수 있습니다.

int n{5}, m{n};
std::cout << "n == m : " << (n == m); // 1이 출력 // 또는 true가 출력

n은 5로 초기화했고, m은 n을 복사해서 만들었으니 당연히 같겠죠?

참고로 여러 개의 관계 연산자가 있을 경우엔 >, >=, <, <=를 먼저 비교하고 ==, !=를 비교합니다.

 

논리 연산자

쉽게 말해서, AND, OR, NOT의 C++식 표현입니다.

AND는 &&, OR은 ||, NOT은 !로 표현합니다.

||는 여러분들 역슬래쉬 \ 있죠? 역슬래쉬를 쉬프트 누르면서 입력하면 |이 됩니다.

아무튼 &&는 양쪽 둘 다 참이어야만 참,

||는 둘 중 하나만 참이어도 참,

!는 참과 거짓을 반전시킵니다.

예제 코드를 보시죠.

#include <iostream>
#include <iomanip>

int main() 
{
    std::cout << std::boolalpha;

    std::cout << "true && false: " << (true && false) << std::endl;
    std::cout << "true || false: " << (true || false) << std::endl;
    std::cout << "!true: " << !true << std::endl << std::endl;

    std::cout << "true && true: " << (true && true) << std::endl;
    std::cout << "false || false: " << (false || false) << std::endl;
    std::cout << "!false: " << !false << std::endl;
    return 0;
}
true && false: false
true || false: true
!true: false

true && true: true
false || false: false
!false: true

관계 연산자의 반환값이 true 또는 false라고 말씀드렸습니다. 저 위의 소스코드에서 true 또는 false라고 되어있는 부분에 관계 연산자를 사용해도 같은 값이 나옵니다.

constexpr auto is_true{1 < 3 && 4 > 2};

이런 식으로 말이죠. 일단 &&, ||가 있으면 그 주변의 관계 연산자를 미리 계산합니다. 자동으로 괄호가 아래처럼 생성됩니다.

(1 < 3) && (4 > 2)

즉, 우리는 저 괄호를 쓰지 않아도 된다는 것입니다.

&&와 ||을 섞어 쓸 수도 있습니다.

true && false || false && false

위 식의 값은 무엇일까요?

&&보다 ||의 순위가 낮기 때문에 &&이 먼저 계산됩니다.

false || false

즉, 위 식의 값은 false네요.

논리 연산자는 후에 배울 반복문이나 제어문을 쓰다보면 금방 익숙해지실 겁니다.

 

증감 연산자 (증가/감소 연산자)

정수형 변수 n을 10으로 초기화하고, 1을 더해봅시다.

int n{10};
n = n + 1; // n = 11

자, 그런데 이런 경우엔 복합 대입 연산자를 사용해서 식을 줄일 수 있다고 했죠?

int n{10};
n += 1; // n = 11;

그런데 이젠 이것도 귀찮습니다. 1만큼만 더하고, 1만큼만 빼기 위해 증감연산자 ++와 --이 존재합니다.

int C{10};
C++; // C = 11

눈치채셨나요? 네. C++의 ++이 바로 증감연산자입니다. C의 값을 1 증가시키라는 뜻입니다.

++은 플러스플러스라고 부르며, 플플 또는 뿔뿔(...)이라고도 부릅니다.

--는 마이너스 마이너스이고, 마마라고도 부릅니다.

그런데, 증감 연산자는 변수의 앞에 올 수도 있고 뒤에 올 수도 있습니다. 그 차이를 한 번 확인해 봅시다.

int i{10}, j{i};

std::printf("i = %d, j = %d\n", i, j);
std::cout << "++i = " << ++i << std::endl;
std::cout << "j++ = " << j++ << std::endl;
std::printf("i = %d, j = %d\n", i, j);
i = 10, j = 10
++i = 11
j++ = 10
i = 11, j = 11

아, 앞에 ++을 쓴 i와 뒤에 ++을 쓴 j의 출력값이 서로 다릅니다?

++i 처럼 ++이 앞에 붙었을 땐 전위 연산자라고 부르며, 그 줄이 실행되기 전에 먼저 1이 증가됩니다.

그에 반해, j++ 처럼 ++이 뒤에 붙었을 땐 후위 연산자라고 부르고, 그 줄이 실행되고 난 후 마지막에 1을 증가시킵니다.

i는 전위연산자를 사용하여 먼저 1이 증가되고 그 값을 출력했고, j는 후위연산자를 사용하여 먼저 출력이 된 다음 증가가 된 것이지요.

그래서 최종적인 값은 같습니다.

그 외에도 큰 차이가 있는데, 전위 연산자는 l-value를 반환하고, 후위 연산자는 r-value를 반환합니다.

++++++i; // 이건 가능합니다.
j++++++; // 에러: 이런 건 불가능합니다.

전위 연산자를 사용할 경우엔 ++을 계속 이어붙일 수 있습니다. 하지만 후위 연산자는 그렇게 사용이 불가능합니다. 일단 쉽게말해서 l-value는 대입이 가능한 값이고, r-value는 대입이 불가능한 값 입니다.

++i += 20; // 가능, i에 1을 더하고, 20을 더합니다.
j++ += 20; // 에러: r-value에 대입할 수 없습니다.

즉, 후위 연산자로 i++, j++이렇게 사용하면 거기에 값을 대입할 수 없다는 것만 알고계시면 됩니다. 하지만 읽기는 가능합니다.

int i, j{5}; // i = ?, j = 5
i = j++;     // i = 5가 대입됨, 그 후에 j가 1 증가됨
i; j;        // i = 5, j = 6;

이렇게 후위 연산을 사용하면 그 줄이 끝나고 연산이 진행됩니다. 저걸 ++j로 바꾸면 i에도 6이 들어갑니다.

여기서 잠깐!

bool 변수는 true와 false만 담을 수 있다고 했습니다. 이 둘은 각각 int형의 1, 0에 해당하는데요,

다음과 같은 코드는 어떤 결과를 낼까요?

bool alpha{false};
alpha++;
std::cout << alpha << std::endl;

정답은 "컴파일 오류가 발생한다!" 입니다. 사실 C++14까지만 해도 1 또는 true로 값이 변경이 되었는데, C++17에 오면서 bool 값에 대한 ++, -- 연산자를 표준에서 제외시켜버렸습니다.

main.cpp: In function ‘int main()’:
main.cpp:5:10: error: use of an operand of type ‘bool’ in ‘operator++’ is forbidden in C++17
     alpha++;
          ^~

 비주얼 스튜디오에서는 아직 지원이 되지만, C++17 표준을 "잘" 지키는 컴파일러들은 이렇게 오류를 발생시킵니다. 지원이 되든 안 되든 사용하지 마시길 바랍니다.

 

삼항 조건 연산자

삼항 연산자라고도 부릅니다. 앞서 배운 관계 연산자와 논리 연산자를 여기서 응용합니다.

(조건식) ? (참일 때 실행할 부분) : (거짓일 때 실행할 부분)

위의 조건식은 bool값을 나타내는 식이 필요합니다. bool 변수를 넣어도 되고 관계, 논리 연산자의 식을 넣어도 됩니다.

int main()
{
    int input;
    std::cout << "숫자를 입력하세요: ";
    std::cin >> input;

    input == 3 ? std::cout << "정답입니다!" : std::cout << "오답입니다.";
}
숫자를 입력하세요: 3
정답입니다!

숫자를 입력받아서 input에 저장합니다. 그리고 이 값을 3과 비교합니다. 만약 3이면 정답입니다,라는 말을 출력하고 3이 아니라면 오답입니다,라는 말을 출력합니다.

input == 3                           // input을 3과 비교합니다.
    ? std::cout << "정답입니다!"      // 참이라면 여기를 실행합니다.
    : std::cout << "오답입니다.";     // 거짓이라면 여기를 실행합니다.

즉, 모두 실행하는 게 아니라 조건식에 따라 실행할 수식을 결정하는 것입니다. 참고로 저기서 공통되는 부분은 앞으로 꺼낼 수 있습니다.

std::cout << (input == 3) ? "정답입니다!" : "오답입니다.";

 (std::cout와 관계 연산자를 한 줄에 쓸 때는 괄호가 필요합니다) input이 3이면 "정답입니다"를, 아니면 "오답입니다"를 출력합니다. 내용은 똑같습니다.

윗줄에서 사용자가 0~9의 숫자가 아닌 그 밖의 범위를 입력할 수도 있겠죠? 입력받는 값의 범위를 제한할 수도 있습니다.

std::cin >> input;
input < 0 || input > 9 ? std::exit(1) : (void)0;
std::cout << (input == 3) ? "정답입니다!" : "오답입니다.";

상당히 좋지 않은 코드이긴 한데, 앞에만 보시면 input이 0보다 작거나 ("또는") 9보다 크면 std::exit()함수를 호출해서 프로그램을 아예 종료해 버립니다. 뒤에 있는 (void)0은 무시하세요. 아무것도 하지 않는 코드입니다. 사실 위의 경우에는 아래처럼 if문을 사용하는 게 일반적입니다.

if (input < 0 || input > 9)
{
    std::cerr << "잘못된 입력값입니다" << std::endl;
    return 1;
}

std::cout대신 std::cerr을 사용했습니다. 일반적인 출력은 std::cout에 하고, 오류 출력은 std::cerr에 하는 게 관례입니다. return 1; 이건 exit(1)과 같은 코드입니다. 프로그램을 종료하고 비정상 종료라는 값을 운영체제에게 넘겨주는 겁니다.

아무튼, 이렇게 삼항 조건 연산자를 사용하면 조건에 맞게 실행할 코드를 정할 수 있습니다.

 

2-2. 도전 과제

1. 다음 수식의 값을 구하시오

1 + 2 * 3 - 4

 

2. 다음 중 결과값이 나머지 넷 과 다른 것은?

(1) 1 <= 2  (2) 3 > 2  (3) 1 == 1  (4) 2 != 5 (5) 1 < 1

 

3. 다음 수식의 값을 구하시오

!(true && false || (false || true) || false && false) && true || !false

 

4. 다음 소스코드의 출력값은 무엇입니까?

int n{5};
std::cout << n++ << std::endl;

 

5. [고난이도] 다음 코드가 실행된 후 i, j의 값은 각각 무엇입니까?

int i{3}, j{5};
(++++i == j ? i : j) = i + j++;

 


수고하셨습니다. 오늘 배운 연산자는 C++ 프로그램의 대부분을 차지한다고 해도 과언이 아닐 정도로 프로그램은 수많은 연산자들로 이루어져 있습니다.

심지어 우리가 std::cout << "Hello, world!"; 를 출력할 때 <<도 연산자입니다! 제가 지금까지는 연산자라는 말을 사용하지 않고 << 라고만 했지만, 정확한 이름은 비트 쉬프트 연산자입니다. 하지만 기능이 살짝 다른데, 내일 이어지는 강좌와 연산자 오버로딩 때 더 자세히 설명하겠습니다.

오늘의 도전과제는 우선 머리속으로 생각하거나 낙서장에 필기하며 계산해보고, 실제로 프로그래밍했을 때 나오는 값과 비교해 보세요. 원활한 프로그래밍을 위해서라도 오늘 배운 내용은 완벽하게 이해하고 넘어가야 합니다.

반응형