산술 연산자
연산자와 피연산자, 표현식의 관계를 정리하고, 사칙연산과 나머지 연산이 피연산자의 타입에 따라 내는 결과, 음수의 정수 나눗셈과 0으로 나누기의 문제, 전위와 후위 증감 연산자가 돌려주는 값과 변수가 바뀌는 시점, 우선순위와 결합 규칙을 다룹니다.
변수에 값을 담았다면, 이제 그 값으로 계산할 차례입니다. 연산자(operator)는 덧셈이나 비교 같은 연산을 하라고 컴파일러에게 알리는 기호로, +, -, *, /처럼 수학에서 보던 것도 있고 %나 ++처럼 C++에서 처음 보는 것도 있습니다.
이 글은 숫자를 계산하는 산술 연산자를 다룹니다. 같은 /라도 피연산자의 타입에 따라 결과가 달라지는 이유와, 증감 연산자가 변수의 값을 바꾸면서 동시에 어떤 값을 돌려주는지, 여러 연산자가 섞인 식이 어떻게 묶이는지를 봅니다.
연산자와 표현식
int result = 10 + 20;에서 +는 연산자이고, 그 대상인 10과 20은 피연산자(operand)이며, 둘을 묶은 10 + 20은 표현식(expression)입니다. 표현식은 계산해서 값을 만들거나, 변수의 값을 바꾸는 것 같은 부수 효과(side effect)를 일으키는 코드 조각으로, 모든 표현식은 타입을 가집니다.
Expression reading contract
산술식은 묶임·전제·타입·부수 효과를 함께 읽는다
기호만 따라 계산하지 않고, 각 연산이 성립하는 조건과 결과 타입까지 함께 판정합니다.
-
구문을 묶습니다
5 + 3 * 2는5 + (3 * 2)로 묶입니다. 우선순위와 결합 규칙은 구문 구조를 정할 뿐, 일반적인 피연산자 평가 순서를 정하지 않습니다. -
연산 전제를 확인합니다
%의 두 피연산자는 정수형 또는 범위 없는 열거형이어야 하고,/와%의 제수는 0이 아니어야 합니다. 증감 대상은 수정 가능한 lvalue여야 합니다. -
공통 타입을 결정합니다
이항 산술 연산은 정수 승격과 통상 산술 변환을 거쳐 공통 타입을 정합니다.
int / int는 정수이고,int / double은 이 예에서double입니다. -
값·타입·부수 효과를 판정합니다
정수 나눗셈은 몫을 0 방향으로 버립니다. 결과를 표현할 수 없는 부호 있는 정수 산술이나 허용되지 않은 나눗셈은 값이 아니라 정의되지 않은 동작의 경계입니다.
그림처럼 산술식은 네 가지를 차례로 확인하며 읽습니다. 먼저 우선순위로 어느 연산자가 어느 피연산자와 묶이는지 정하고, 0으로 나누는 것처럼 연산이 성립하지 않는 입력이 없는지 확인합니다. 그다음 각 부분식의 결과 타입을 정하고, 마지막으로 결과 값과 변수가 바뀌는 효과를 봅니다. 이 순서는 식을 읽는 순서일 뿐, 실제로 피연산자를 계산하는 순서는 아닙니다.
사칙연산과 나머지
C++의 기본 산술 연산자는 다섯 가지입니다.
| 연산자 | 뜻 | 예 | 결과 |
|---|---|---|---|
+ | 덧셈 | 5 + 3 | 8 |
- | 뺄셈 | 5 - 3 | 2 |
* | 곱셈 | 5 * 3 | 15 |
/ | 나눗셈 | 5 / 3 | 1 |
% | 나머지 | 5 % 3 | 2 |
5 / 3이 1인 것은 두 피연산자가 모두 정수여서 정수 나눗셈을 하기 때문입니다. 정수 나눗셈은 몫의 소수점 아래를 0 쪽으로 버리므로, 양수에서는 내림과 같지만 음수에서는 -10 / 3이 -4가 아니라 -3이 됩니다. 피연산자 가운데 하나라도 실수이면 형변환에서 본 일반 산술 변환으로 둘 다 실수가 되어 실수 나눗셈을 합니다.
나머지 연산자 %는 정수에만 쓸 수 있으며, 10.5 % 3.2처럼 실수에 쓰면 컴파일 오류가 납니다. 실수의 나머지가 필요하면 <cmath>의 std::fmod를 씁니다. 몫을 표현할 수 있는 정수에서는 언제나 (a / b) * b + a % b가 a와 같도록 정해져 있어서, 나머지의 부호는 나누어지는 수 a의 부호를 따릅니다.
#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.5c / 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이다. 입력·식 결과·저장 상태를 병렬 비교한다.
Prefix vs postfix
전위는 바뀐 객체를, 후위는 이전 값을 결과로 만든다
둘 다 저장된 값을 6으로 만들지만, y 초기화에 사용되는 표현식 결과는 다릅니다.
- 전위
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
bool은 허용되지 않습니다. 이전 값이 필요하지 않다면 의도가 드러나는 형식을 고르고, 여러 부수 효과를 한 식에 겹치지 않습니다.이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 도표 제목 | 같은 5에서 전위와 후위의 결과를 각각 추적한다 |
| 관계·조건 설명 | 두 실험은 각각 x 5에서 시작한다. 전위 식은 6을 식 값으로 주어 문장 끝 x 6, y 6을 만들고, 후위 식은 이전 값 5를 주어 문장 끝 x 6, y 5를 만든다. 문장 종료 상태와 식 결과를 분리하며 여러 증감을 한 식에 결합한 예가 아니다. |
| 노드·연결선 라벨 | 변경된 값 6 |
| 노드·연결선 라벨 | 이전 값 5 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 독립 사본 준비 전위와 후위의 독립 실험 모두 x=5에서 시작한다. |
|
|
| 식 평가 증가 전후 반환값을 구분한다. |
|
|
| 저장 상태 두 실험의 증가가 끝난 뒤 x를 읽는다. |
|
|
C++ 전위와 후위 증가의 결과 비교
독립된 x=5 실험에서 전위는 식 값 6, 후위는 식 값 5이고 최종 x는 둘 다 6이다. 입력·식 결과·저장 상태를 병렬 비교한다.
Prefix vs postfix
전위는 바뀐 객체를, 후위는 이전 값을 결과로 만든다
둘 다 저장된 값을 6으로 만들지만, y 초기화에 사용되는 표현식 결과는 다릅니다.
- 전위
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
bool은 허용되지 않습니다. 이전 값이 필요하지 않다면 의도가 드러나는 형식을 고르고, 여러 부수 효과를 한 식에 겹치지 않습니다.이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 도표 제목 | 같은 5에서 전위와 후위의 결과를 각각 추적한다 |
| 관계·조건 설명 | 두 실험은 각각 x 5에서 시작한다. 전위 식은 6을 식 값으로 주어 문장 끝 x 6, y 6을 만들고, 후위 식은 이전 값 5를 주어 문장 끝 x 6, y 5를 만든다. 문장 종료 상태와 식 결과를 분리하며 여러 증감을 한 식에 결합한 예가 아니다. |
| 노드·연결선 라벨 | 변경된 값 6 |
| 노드·연결선 라벨 | 이전 값 5 |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 독립 사본 준비 전위와 후위의 독립 실험 모두 x=5에서 시작한다. |
|
|
| 식 평가 증가 전후 반환값을 구분한다. |
|
|
| 저장 상태 두 실험의 증가가 끝난 뒤 x를 읽는다. |
|
|
그림처럼 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를 이번 식에서 계산된 값으로 읽으면 충분하며, 값 범주는 뒤의 장에서 자세히 봅니다.
#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인지 검사해 따로 처리합니다.
다음 글 관계 연산자와 논리 연산자에서는 두 값을 비교하는 관계 연산자와, 여러 조건을 잇는 논리 연산자, 그리고 논리 연산자가 오른쪽을 계산하지 않고 끝내는 단락 평가를 다룹니다.