본문 바로가기

산술 연산자

연산자와 피연산자, 표현식의 관계를 정리하고, 사칙연산과 나머지 연산이 피연산자의 타입에 따라 내는 결과, 음수의 정수 나눗셈과 0으로 나누기의 문제, 전위와 후위 증감 연산자가 돌려주는 값과 변수가 바뀌는 시점, 우선순위와 결합 규칙을 다룹니다.

변수에 값을 담았다면, 이제 그 값으로 계산할 차례입니다. 연산자(operator)는 덧셈이나 비교 같은 연산을 하라고 컴파일러에게 알리는 기호로, +, -, *, /처럼 수학에서 보던 것도 있고 %나 ++처럼 C++에서 처음 보는 것도 있습니다.

이 글은 숫자를 계산하는 산술 연산자를 다룹니다. 같은 /라도 피연산자의 타입에 따라 결과가 달라지는 이유와, 증감 연산자가 변수의 값을 바꾸면서 동시에 어떤 값을 돌려주는지, 여러 연산자가 섞인 식이 어떻게 묶이는지를 봅니다.

연산자와 표현식

int result = 10 + 20;에서 +는 연산자이고, 그 대상인 10과 20은 피연산자(operand)이며, 둘을 묶은 10 + 20은 표현식(expression)입니다. 표현식은 계산해서 값을 만들거나, 변수의 값을 바꾸는 것 같은 부수 효과(side effect)를 일으키는 코드 조각으로, 모든 표현식은 타입을 가집니다.

C++ 산술식의 구문 묶음, 연산 전제, 공통 타입, 값과 부수 효과를 순서대로 확인하는 네 단계 점검표

Expression reading contract

산술식은 묶임·전제·타입·부수 효과를 함께 읽는다

기호만 따라 계산하지 않고, 각 연산이 성립하는 조건과 결과 타입까지 함께 판정합니다.

산술식을 읽는 네 점검 단계구문 묶음을 정한 뒤 연산의 허용 입력을 확인하고 부분식 타입을 결정한 다음 결과값과 객체 변화를 판정한다. 이 점검 순서는 일반 피연산자의 실행 순서를 뜻하지 않는다.
구문 묶음5 + (3 * 2)
연결 관계구문 묶음 · 5 + (3 * 2) → 전제 확인 · 제수 · 범위 · 대상
전제 확인제수 · 범위 · 대상
연결 관계전제 확인 · 제수 · 범위 · 대상 → 공통 타입 · 각 부분식에서 결정
공통 타입각 부분식에서 결정
연결 관계공통 타입 · 각 부분식에서 결정 → 값 · 저장 변화 · 결과와 부수 효과
값 · 저장 변화결과와 부수 효과
  1. 구문을 묶습니다

    5 + 3 * 2는 5 + (3 * 2)로 묶입니다. 우선순위와 결합 규칙은 구문 구조를 정할 뿐, 일반적인 피연산자 평가 순서를 정하지 않습니다.

  2. 연산 전제를 확인합니다

    %의 두 피연산자는 정수형 또는 범위 없는 열거형이어야 하고, /와 %의 제수는 0이 아니어야 합니다. 증감 대상은 수정 가능한 lvalue여야 합니다.

  3. 공통 타입을 결정합니다

    이항 산술 연산은 정수 승격과 통상 산술 변환을 거쳐 공통 타입을 정합니다. int / int는 정수이고, int / double은 이 예에서 double입니다.

  4. 값·타입·부수 효과를 판정합니다

    정수 나눗셈은 몫을 0 방향으로 버립니다. 결과를 표현할 수 없는 부호 있는 정수 산술이나 허용되지 않은 나눗셈은 값이 아니라 정의되지 않은 동작의 경계입니다.

괄호로 묶음을 명시하고, 같은 객체를 여러 번 읽고 바꾸는 복잡한 식은 단순한 문장으로 나눕니다.

그림처럼 산술식은 네 가지를 차례로 확인하며 읽습니다. 먼저 우선순위로 어느 연산자가 어느 피연산자와 묶이는지 정하고, 0으로 나누는 것처럼 연산이 성립하지 않는 입력이 없는지 확인합니다. 그다음 각 부분식의 결과 타입을 정하고, 마지막으로 결과 값과 변수가 바뀌는 효과를 봅니다. 이 순서는 식을 읽는 순서일 뿐, 실제로 피연산자를 계산하는 순서는 아닙니다.

사칙연산과 나머지

C++의 기본 산술 연산자는 다섯 가지입니다.

연산자뜻예결과
+덧셈5 + 38
-뺄셈5 - 32
*곱셈5 * 315
/나눗셈5 / 31
%나머지5 % 32

5 / 3이 1인 것은 두 피연산자가 모두 정수여서 정수 나눗셈을 하기 때문입니다. 정수 나눗셈은 몫의 소수점 아래를 0 쪽으로 버리므로, 양수에서는 내림과 같지만 음수에서는 -10 / 3이 -4가 아니라 -3이 됩니다. 피연산자 가운데 하나라도 실수이면 형변환에서 본 일반 산술 변환으로 둘 다 실수가 되어 실수 나눗셈을 합니다.

나머지 연산자 %는 정수에만 쓸 수 있으며, 10.5 % 3.2처럼 실수에 쓰면 컴파일 오류가 납니다. 실수의 나머지가 필요하면 <cmath>의 std::fmod를 씁니다. 몫을 표현할 수 있는 정수에서는 언제나 (a / b) * b + a % b가 a와 같도록 정해져 있어서, 나머지의 부호는 나누어지는 수 a의 부호를 따릅니다.

divide_and_mod.cpp
#include <iostream>

int main() {
    int a = 10;
    int b = 3;
    double c = 10.0;
    double d = 3.0;

    std::cout << "a / b: " << a / b << "\n";
    std::cout << "a % b: " << a % b << "\n";
    std::cout << "c / d: " << c / d << "\n";
    std::cout << "a / d: " << a / d << "\n";
    std::cout << "-10 / 3: " << -10 / 3 << ", -10 % 3: " << -10 % 3 << "\n";

    int total_items = 7;
    int num_boxes = 2;
    std::cout << "per box: " << static_cast<double>(total_items) / num_boxes << "\n";
}
a / b: 3
a % b: 1
c / d: 3.33333
a / d: 3.33333
-10 / 3: -3, -10 % 3: -1
per box: 3.5

c / d가 3.33333으로 보이는 것은 std::cout이 기본으로 유효 숫자 6자리까지 보여 주기 때문이며, 저장된 값은 더 긴 이진 근삿값입니다. a / d는 a가 double로 바뀌어 같은 결과가 나오고, 7과 2를 나눈 마지막 줄은 나누기 전에 하나를 double로 바꾸어 3.5를 얻었습니다. -10 % 3이 -1인 것은 몫이 -3이어서 (-3) * 3 + (-1)이 -10이 되기 때문입니다.

/와 %의 오른쪽 피연산자가 0이면 정의되지 않은 동작이므로, 나누기 전에 제수가 0이 아닌지 확인합니다. 또 int의 최솟값을 -1로 나누는 것처럼 몫이 그 타입의 범위를 넘는 경우도 정의되지 않은 동작입니다. 정수 연산의 결과가 범위를 넘으면 부호 있는 타입에서는 정의되지 않은 동작, 부호 없는 타입에서는 순환한다는 것은 덧셈과 곱셈에서도 마찬가지입니다.

단항 연산자

피연산자가 하나인 단항(unary) 연산자 -는 부호를 바꾸고, 단항 +는 값을 그대로 둡니다. int num = 10;에서 -num은 -10이고, -(-num)은 다시 10입니다. 단항 +는 거의 쓰지 않지만, char 같은 작은 정수에 붙이면 int로 승격된 값이 되어 글자 대신 숫자로 출력하는 데 쓰기도 합니다.

증감 연산자

++는 변수의 값을 1 늘리고 --는 1 줄입니다. 이 연산자는 변수의 앞에 붙이는 전위(prefix) ++x와 뒤에 붙이는 후위(postfix) x++가 있으며, 둘 다 변수를 1 늘리지만 식의 결과로 돌려주는 값이 다릅니다.

C++ 전위와 후위 증가의 결과 비교

독립된 x=5 실험에서 전위는 식 값 6, 후위는 식 값 5이고 최종 x는 둘 다 6이다. 입력·식 결과·저장 상태를 병렬 비교한다.

초깃값이 5일 때 C++ 내장 전위 증가와 후위 증가가 만드는 표현식 결과, 저장 결과, 값 범주, 순서 관계 비교

Prefix vs postfix

전위는 바뀐 객체를, 후위는 이전 값을 결과로 만든다

둘 다 저장된 값을 6으로 만들지만, y 초기화에 사용되는 표현식 결과는 다릅니다.

같은5에서 전위와 후위의 결과를 각각 추적한다두 실험은 각각x5에서 시작한다. 전위 식은6을주어문장끝x6y6,후위식은이전5를주어문장끝x6y5를만든다. 문장 종료 상태와 식 결과를 분리하며 여러 증감을 한식에 결합한예가 아니다.전위: int y = ++x;시작 객체x = 5표현식 결과변경된 값6문장 종료x = 6 · y = 6후위: int y = x++;시작 객체x = 5표현식 결과이전 값5문장 종료x = 6 · y = 5
같은5에서 전위와 후위의 결과를 각각 추적한다두 실험은 각각x5에서 시작한다. 전위 식은6을주어문장끝x6y6,후위식은이전5를주어문장끝x6y5를만든다. 문장 종료 상태와 식 결과를 분리하며 여러 증감을 한식에 결합한예가 아니다.전위: int y = ++x;시작 객체x = 5표현식 결과변경된 값6문장 종료x = 6 · y = 6후위: int y = x++;시작 객체x = 5표현식 결과이전 값5문장 종료x = 6 · y = 5
전위 int y = ++x;
초깃값 x = 5
의미 내장 연산에서는 x += 1과 같습니다.
표현식 결과 변경된 객체를 나타내므로 값은 6, 값 범주는 lvalue입니다.
문장 종료 후 x = 6, y = 6
후위 int y = x++;
초깃값 x = 5
의미 변경 전 값을 표현식 결과로 만듭니다.
표현식 결과 이전 값 5의 prvalue이며, 이 값 계산은 x의 변경보다 먼저 순서화됩니다.
문장 종료 후 x = 6, y = 5
내장 증감 연산의 대상은 수정 가능한 lvalue여야 하며 bool은 허용되지 않습니다. 이전 값이 필요하지 않다면 의도가 드러나는 형식을 고르고, 여러 부수 효과를 한 식에 겹치지 않습니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
도표 제목같은 5에서 전위와 후위의 결과를 각각 추적한다
관계·조건 설명두 실험은 각각 x 5에서 시작한다. 전위 식은 6을 식 값으로 주어 문장 끝 x 6, y 6을 만들고, 후위 식은 이전 값 5를 주어 문장 끝 x 6, y 5를 만든다. 문장 종료 상태와 식 결과를 분리하며 여러 증감을 한 식에 결합한 예가 아니다.
노드·연결선 라벨변경된 값 6
노드·연결선 라벨이전 값 5
예의 값과 변화
관찰·상황값·보충 정보변화·이유
독립 사본 준비

전위와 후위의 독립 실험 모두 x=5에서 시작한다.

  • 전위 x: 5
  • 후위 x: 5
  • 두 실험은 같은 x에 연속 적용하지 않는다.
식 평가

증가 전후 반환값을 구분한다.

  • ++x의 식 값: 6
  • x++의 식 값: 5
  • 전위는 증가한 값, 후위는 증가 전 값을 식의 결과로 낸다.
저장 상태

두 실험의 증가가 끝난 뒤 x를 읽는다.

  • 전위 최종 x: 6
  • 후위 최종 x: 6
  • 식의 결과가 달라도 증가 후 원본 저장값은 같다.

C++ 전위와 후위 증가의 결과 비교

독립된 x=5 실험에서 전위는 식 값 6, 후위는 식 값 5이고 최종 x는 둘 다 6이다. 입력·식 결과·저장 상태를 병렬 비교한다.

초깃값이 5일 때 C++ 내장 전위 증가와 후위 증가가 만드는 표현식 결과, 저장 결과, 값 범주, 순서 관계 비교

Prefix vs postfix

전위는 바뀐 객체를, 후위는 이전 값을 결과로 만든다

둘 다 저장된 값을 6으로 만들지만, y 초기화에 사용되는 표현식 결과는 다릅니다.

같은5에서 전위와 후위의 결과를 각각 추적한다두 실험은 각각x5에서 시작한다. 전위 식은6을주어문장끝x6y6,후위식은이전5를주어문장끝x6y5를만든다. 문장 종료 상태와 식 결과를 분리하며 여러 증감을 한식에 결합한예가 아니다.전위: int y = ++x;시작 객체x = 5표현식 결과변경된 값6문장 종료x = 6 · y = 6후위: int y = x++;시작 객체x = 5표현식 결과이전 값5문장 종료x = 6 · y = 5
같은5에서 전위와 후위의 결과를 각각 추적한다두 실험은 각각x5에서 시작한다. 전위 식은6을주어문장끝x6y6,후위식은이전5를주어문장끝x6y5를만든다. 문장 종료 상태와 식 결과를 분리하며 여러 증감을 한식에 결합한예가 아니다.전위: int y = ++x;시작 객체x = 5표현식 결과변경된 값6문장 종료x = 6 · y = 6후위: int y = x++;시작 객체x = 5표현식 결과이전 값5문장 종료x = 6 · y = 5
전위 int y = ++x;
초깃값 x = 5
의미 내장 연산에서는 x += 1과 같습니다.
표현식 결과 변경된 객체를 나타내므로 값은 6, 값 범주는 lvalue입니다.
문장 종료 후 x = 6, y = 6
후위 int y = x++;
초깃값 x = 5
의미 변경 전 값을 표현식 결과로 만듭니다.
표현식 결과 이전 값 5의 prvalue이며, 이 값 계산은 x의 변경보다 먼저 순서화됩니다.
문장 종료 후 x = 6, y = 5
내장 증감 연산의 대상은 수정 가능한 lvalue여야 하며 bool은 허용되지 않습니다. 이전 값이 필요하지 않다면 의도가 드러나는 형식을 고르고, 여러 부수 효과를 한 식에 겹치지 않습니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
도표 제목같은 5에서 전위와 후위의 결과를 각각 추적한다
관계·조건 설명두 실험은 각각 x 5에서 시작한다. 전위 식은 6을 식 값으로 주어 문장 끝 x 6, y 6을 만들고, 후위 식은 이전 값 5를 주어 문장 끝 x 6, y 5를 만든다. 문장 종료 상태와 식 결과를 분리하며 여러 증감을 한 식에 결합한 예가 아니다.
노드·연결선 라벨변경된 값 6
노드·연결선 라벨이전 값 5
예의 값과 변화
관찰·상황값·보충 정보변화·이유
독립 사본 준비

전위와 후위의 독립 실험 모두 x=5에서 시작한다.

  • 전위 x: 5
  • 후위 x: 5
  • 두 실험은 같은 x에 연속 적용하지 않는다.
식 평가

증가 전후 반환값을 구분한다.

  • ++x의 식 값: 6
  • x++의 식 값: 5
  • 전위는 증가한 값, 후위는 증가 전 값을 식의 결과로 낸다.
저장 상태

두 실험의 증가가 끝난 뒤 x를 읽는다.

  • 전위 최종 x: 6
  • 후위 최종 x: 6
  • 식의 결과가 달라도 증가 후 원본 저장값은 같다.

그림처럼 x가 5일 때 전위 int y = ++x;는 x를 6으로 바꾸고 바뀐 x 자신을 결과로 주어 y도 6이 됩니다. 후위 int y = x++;는 바꾸기 전의 값 5를 결과로 주고 x를 6으로 바꾸므로, 문장이 끝나면 x는 6이고 y는 5입니다. 두 경우 모두 문장이 끝나면 x는 6이며, 다른 것은 그 식이 내놓은 값입니다.

전위의 결과는 바뀐 변수 그 자체를 가리키는 lvalue이고, 후위의 결과는 바뀌기 전의 값을 담은 임시 값인 prvalue입니다. 지금은 lvalue를 계속 존재하는 객체를 가리키는 식으로, prvalue를 이번 식에서 계산된 값으로 읽으면 충분하며, 값 범주는 뒤의 장에서 자세히 봅니다.

ticket.cpp
#include <iostream>

int main() {
    int ticket{5};
    int issued{ticket++};
    int next{++ticket};
    std::cout << issued << ' ' << next << ' ' << ticket << '\n';
}
5 7 7

첫 증감은 issued에 바꾸기 전의 5를 주고 ticket을 6으로 바꿉니다. 둘째 증감은 ticket을 7로 바꾼 뒤 그 값을 next에 줍니다. 증감은 이렇게 한 문장에 하나씩 두어, 같은 변수를 한 식에서 여러 번 바꾸거나 읽지 않게 합니다. i = i++ + ++i;처럼 +의 두 피연산자가 같은 변수를 바꾸는 식은 표준 버전과 상관없이 정의되지 않은 동작이어서, 어떤 결과가 나올지 믿을 수 없습니다.

값을 1 바꾸는 것만이 목적이라면 전위와 후위는 결과가 같습니다. int 같은 기본 타입에서는 성능의 차이도 거의 없지만, 뒤에서 배울 반복자나 클래스의 후위 증가는 바꾸기 전의 값을 복사해 두어야 할 수 있어서, 이전 값이 필요 없을 때는 전위를 쓰는 습관이 좋습니다. 증감 연산자는 바꿀 수 있는 변수에만 쓸 수 있고, bool에는 쓸 수 없습니다.

우선순위와 결합 규칙

한 식에 여러 연산자가 있으면, 연산자 우선순위(operator precedence)가 어느 연산자가 피연산자와 먼저 묶이는지를 정합니다.

우선순위연산자결합 방향
높음후위 x++, x--왼쪽에서 오른쪽
전위 ++x, --x, 단항 +x, -x오른쪽에서 왼쪽
*, /, %왼쪽에서 오른쪽
+, -왼쪽에서 오른쪽
낮음대입 =오른쪽에서 왼쪽

5 + 3 * 2는 곱셈이 덧셈보다 우선순위가 높아 5 + (3 * 2)로 묶여 11입니다. 우선순위가 같은 연산자가 이어지면 결합 방향(associativity)이 어느 쪽부터 묶을지를 정하며, 이항 산술 연산자는 왼쪽에서 오른쪽으로 결합하므로 10 - 5 + 2는 (10 - 5) + 2인 7입니다. 다른 묶음이 필요하면 괄호를 쓰며, (5 + 3) * 2는 16입니다.

우선순위는 식이 어떻게 묶이는지를 정할 뿐, 피연산자를 어떤 순서로 계산하는지를 정하지는 않습니다. f() + g()에서 f와 g 가운데 무엇이 먼저 실행되는지는 정해져 있지 않으므로, 두 함수가 같은 변수를 바꾼다면 결과가 달라질 수 있습니다. 우선순위를 모두 외우기보다, 헷갈릴 만한 식에는 괄호를 넣어 의도를 분명히 합니다.

정리

  • 표현식은 연산자와 피연산자를 묶어 값을 만들거나 부수 효과를 일으키며, 산술식은 묶임과 전제, 타입, 결과와 효과를 차례로 확인하며 읽습니다.
  • 정수끼리의 나눗셈은 몫의 소수점 아래를 0 쪽으로 버리고, 피연산자에 실수가 섞이면 실수 나눗셈을 합니다.
  • %는 정수에만 쓰며, (a / b) * b + a % b가 a와 같도록 나머지의 부호가 나누어지는 수를 따릅니다.
  • 0으로 나누거나 몫이 범위를 넘는 것, 부호 있는 정수의 결과가 범위를 넘는 것은 정의되지 않은 동작입니다.
  • 전위 증감은 바뀐 변수를, 후위 증감은 바뀌기 전의 값을 결과로 주며, 같은 변수를 한 식에서 여러 번 바꾸지 않습니다.
  • 우선순위와 결합 방향은 식이 묶이는 방식을 정할 뿐 계산 순서를 정하지 않으므로, 헷갈리는 식에는 괄호를 씁니다.

연습 문제

1. 전위로 바꾸면

ticket.cpp에서 ticket++를 ++ticket으로 바꾸면 무엇이 출력되나요?

해설 보기

첫 증감이 ticket을 6으로 바꾼 뒤 그 값을 issued에 주므로 issued는 6이 됩니다. 둘째 증감이 7을 만들어 next와 ticket이 7이므로, 6 7 7이 출력됩니다.

2. 음수의 몫과 나머지

a가 -10이고 b가 3일 때 a / b와 a % b는 얼마이고, (a / b) * b + a % b는 얼마인가요? a가 10이고 b가 -3이면 어떻게 되나요?

해설 보기

몫은 0 쪽으로 버린 -3, 나머지는 -1이며, (-3) * 3 + (-1)은 -10이 되어 a와 같습니다. a가 10이고 b가 -3이면 몫은 -3이고, (-3) * (-3) + r이 10이 되어야 하므로 나머지는 1입니다. 나머지의 부호는 나누어지는 수를 따르므로, 음수에서 나머지가 언제나 양수라고 가정하지 않습니다.

3. 평균 구하기

int sum = 7;과 int count = 2;로 평균을 구해 double avg에 담으려 합니다. double avg = sum / count;는 얼마가 되고, 3.5를 얻으려면 어떻게 고쳐야 하나요? count가 0일 수 있다면 무엇을 더해야 하나요?

해설 보기

sum / count가 먼저 정수 나눗셈으로 3을 만든 뒤 double로 바뀌므로 avg는 3.0입니다. static_cast<double>(sum) / count처럼 나누기 전에 하나를 double로 바꾸면 3.5가 됩니다. count가 0이면 정수 나눗셈이 정의되지 않은 동작이 되므로, 나누기 전에 count가 0인지 검사해 따로 처리합니다.

다음 글 관계 연산자와 논리 연산자에서는 두 값을 비교하는 관계 연산자와, 여러 조건을 잇는 논리 연산자, 그리고 논리 연산자가 오른쪽을 계산하지 않고 끝내는 단락 평가를 다룹니다.