산술 연산자
산술 연산자의 피연산자와 결과 타입을 읽고 정수 나눗셈·나머지·증감 연산의 값 변경 시점을 구분합니다.
데이터들을 실제로 움직이고 계산하는 방법에 대해 배울 차례입니다.
그 중심에는 바로 연산자(Operators)가 있습니다.
연산자는 특정 연산을 수행하도록 컴파일러에 지시하는 기호입니다.
이 장에서는 사칙연산을 비롯하여 숫자를 계산하고 조작하는 데 사용되는 산술 연산자(Arithmetic Operators)를 다룹니다.
먼저 산술 연산자를 구문 묶음, 연산 전제, 결과 타입, 값 변경 시점이라는 네 축으로 나눠 봅니다.
연산자와 표현식의 개념
먼저 연산자, 피연산자, 표현식이 어떻게 연결되는지부터 잡습니다.
본격적으로 산술 연산자를 살펴보기 전에, 연산자와 표현식이라는 개념을 명확히 하는 것이 중요합니다.
- 연산자 (Operator): 특정 연산을 수행하도록 컴파일러에 지시하는 기호입니다. (예:
+,-,*,/,%) - 피연산자 (Operand): 연산의 대상이 되는 값이나 변수입니다. (예:
a + b에서a와b) - 표현식 (Expression): 연산자와 피연산자 등을 조합해 계산이나 동작을 기술하는 코드 조각입니다. 표현식의 타입과 값 범주를 판정할 수 있고, 종류에 따라 값이나 부수 효과를 만듭니다. 이 장의
a + b,score * 2,(x + y) / 2같은 산술 표현식은 값을 만듭니다.
int result = 10 + 20; // '10', '20'은 피연산자, '+'는 연산자, '10 + 20'은 표현식, 'result'는 변수산술식은 기호 하나만 보는 것이 아니라 피연산자 타입, 우선순위, 최종 결과 타입을 함께 읽어야 합니다.
Expression reading contract
산술식은 묶임·전제·타입·부수 효과를 함께 읽는다
기호만 따라 계산하지 않고, 각 연산이 성립하는 조건과 결과 타입까지 함께 판정합니다.
-
구문을 묶습니다
5 + 3 * 2는5 + (3 * 2)로 묶입니다. 우선순위와 결합 규칙은 구문 구조를 정할 뿐, 일반적인 피연산자 평가 순서를 정하지 않습니다. -
연산 전제를 확인합니다
%의 두 피연산자는 정수형 또는 범위 없는 열거형이어야 하고,/와%의 제수는 0이 아니어야 합니다. 증감 대상은 수정 가능한 lvalue여야 합니다. -
공통 타입을 결정합니다
이항 산술 연산은 정수 승격과 통상 산술 변환을 거쳐 공통 타입을 정합니다.
int / int는 정수이고,int / double은 이 예에서double입니다. -
값·타입·부수 효과를 판정합니다
정수 나눗셈은 몫을 0 방향으로 버립니다. 결과를 표현할 수 없는 부호 있는 정수 산술이나 허용되지 않은 나눗셈은 값이 아니라 정의되지 않은 동작의 경계입니다.
기본 산술 연산자
C++은 우리가 수학에서 흔히 사용하는 사칙연산을 위한 기본적인 산술 연산자를 제공합니다.
| 연산자 | 이름 | 설명 | 예시 (결과) |
|---|---|---|---|
+ | 덧셈 (Addition) | 두 피연산자를 더합니다. | 5 + 3 (8) |
- | 뺄셈 (Subtraction) | 왼쪽 피연산자에서 오른쪽 피연산자를 뺍니다. | 5 - 3 (2) |
* | 곱셈 (Multiplication) | 두 피연산자를 곱합니다. | 5 * 3 (15) |
/ | 나눗셈 (Division) | 왼쪽 피연산자를 오른쪽 피연산자로 나눕니다. | 5 / 3 (1) |
% | 나머지 (Modulo) | 왼쪽 피연산자를 오른쪽 피연산자로 나눈 나머지 값을 반환합니다. 정수형·범위 없는 열거형 피연산자에 적용됩니다. | 5 % 3 (2) |
/)의 특성 (정수 vs. 실수)
나눗셈 연산자 /는 피연산자의 타입에 따라 다르게 동작합니다.
- 정수 / 정수: 결과도 정수입니다. 몫의 소수 부분은 음수에서도 0 방향으로 버립니다. 따라서
-10 / 3은-3입니다.정수 나눗셈 예시 int a = 10; int b = 3; int result_int = a / b; // 10 / 3 = 3 double result_double = a / b; // 10 / 3 = 3.0 (정수 나눗셈 후 3이 double로 변환) std::cout << "정수 나눗셈: " << result_int << std::endl; // 출력: 3 std::cout << "정수 나눗셈 후 실수 변환: " << result_double << std::endl; // 기본 출력 형식: 3 - 실수 타입이 섞인 나눗셈: 정수 승격과 통상 산술 변환(usual arithmetic conversions)을 거쳐 두 피연산자의 공통 타입을 정합니다. 아래
int와double조합에서는int가double로 변환된 뒤 결과 타입도double이 됩니다.실수 나눗셈 예시 double c = 10.0; double d = 3.0; double result_float = c / d; // 10.0 / 3.0 = 3.333... double mixed_result = a / d; // a(int)가 double로 변환 후 10.0 / 3.0 = 3.333... std::cout << "실수 나눗셈: " << result_float << std::endl; // 출력: 3.333... std::cout << "혼합 나눗셈: " << mixed_result << std::endl; // 출력: 3.333...
몫의 소수부를 버리지 않고 실수 나눗셈을 하려면, 나누기 전에 최소한 하나의 피연산자를 실수 타입으로 명시적 형 변환합니다. 결과가 실수 타입이어도 모든 수를 정확히 표현할 수 있는 것은 아닙니다.
int total_items = 7;
int num_boxes = 2;
double items_per_box = static_cast<double>(total_items) / num_boxes;
std::cout << "상자당 물건 수: " << items_per_box << std::endl; // 출력: 3.5나머지 연산자(%)의 특성:
나머지 연산자는 정수 승격을 적용할 수 있는 정수형 또는 범위 없는 열거형 피연산자에 사용할 수 있습니다.
실수에 사용하면 컴파일 오류가 발생합니다.
int quotient = 10 / 3; // 몫: 3
int remainder = 10 % 3; // 나머지: 1
std::cout << "10 나누기 3의 몫: " << quotient << std::endl; // 출력: 3
std::cout << "10 나누기 3의 나머지: " << remainder << std::endl; // 출력: 1
// double d = 10.5 % 3.2; // 오류! 실수에는 나머지 연산자 사용 불가/와 %의 오른쪽 피연산자가 0이면 동작이 정의되지 않습니다. 또한 부호 있는 정수의 몫을 결과 타입으로 표현할 수 없는 경우(예: 최솟값을 -1로 나누는 경우)에는 /와 % 모두 정의되지 않은 동작입니다. 몫을 표현할 수 있을 때는 (a / b) * b + a % b == a 관계가 성립합니다.
단항 산술 연산자
기본 산술 연산자 외에, 피연산자가 하나인 단항(Unary) 산술 연산자도 있습니다.
| 연산자 | 이름 | 설명 | 예시 (결과) |
|---|---|---|---|
+ | 단항 덧셈 (Unary Plus) | 피연산자의 부호를 유지합니다. (거의 사용 안 함) | +5 (5) |
- | 단항 뺄셈 (Unary Minus) | 피연산자의 부호를 반전시킵니다. | -5 (-5) |
int num = 10;
int positive_num = +num; // num의 값을 그대로 사용
int negative_num = -num; // num의 부호를 반전 (10 -> -10)
int negative_negative_num = -negative_num; // (-10)의 부호를 반전 (-> 10)
std::cout << "단항 +: " << positive_num << std::endl; // 출력: 10
std::cout << "단항 -: " << negative_num << std::endl; // 출력: -10
std::cout << "단항 -를 다시 적용: " << negative_negative_num << std::endl; // 출력: 10증감 연산자
변수의 값을 1 증가시키거나 1 감소시킬 때 사용하는 특수한 연산자입니다.
값 변경 의도를 간결하게 표현하지만, 식 전체에서 어떤 값을 만들어 내는지까지 구분해야 합니다.
| 연산자 | 이름 | 설명 | 예시 (결과) |
|---|---|---|---|
++ | 증가 (Increment) | 피연산자의 값을 1 증가시킵니다. | x++, ++x |
-- | 감소 (Decrement) | 피연산자의 값을 1 감소시킵니다. | x--, --x |
전위(Prefix) vs. 후위(Postfix):
증감 연산자는 피연산자의 앞(++x, --x)에 붙느냐, 뒤(x++, x--)에 붙느냐에 따라 동작 방식에 중요한 차이가 있습니다.
-
전위 연산자 (Prefix Operator):
++x,--x- 내장 전위 증가
++x는x += 1과 같은 의미이며, 변경된 객체를 나타내는 lvalue 결과를 만듭니다.
전위 연산자 예시 int x = 5; int y = ++x; // x를 1 증가시켜 6으로 만든 후, 그 값 6을 y에 할당 std::cout << "x: " << x << ", y: " << y << std::endl; // 출력: x: 6, y: 6 - 내장 전위 증가
-
후위 연산자 (Postfix Operator):
x++,x--- 변경 전 값을 prvalue 결과로 만듭니다. 이 결과의 값 계산은 피연산자 객체를 변경하는 부수 효과보다 먼저 순서화됩니다.
후위 연산자 예시 int a = 5; int b = a++; // a의 현재 값 5를 b에 할당한 후, a를 1 증가시켜 6으로 만듦 std::cout << "a: " << a << ", b: " << b << std::endl; // 출력: a: 6, b: 5
변수 값을 1 증가/감소시키는 목적만 있다면 전위와 후위 모두 결과적으로 변수 값은 동일하게 변합니다.
하지만 다른 연산과 함께 사용할 때는 반드시 두 방식의 차이를 이해하고 써야 합니다.
내장 산술 타입에서는 컴파일러 최적화 후 두 형식의 비용 차이가 보통 의미가 없지만, 반복자나 클래스의 오버로드된 후위 연산은 이전 값 객체를 만들어야 할 수 있습니다. 이전 값이 필요하지 않다면 전위 형식이 의도를 더 직접적으로 드러냅니다.
두 형식 모두 수정 가능한 lvalue를 대상으로 해야 하며 bool에는 사용할 수 없습니다. volatile 피연산자에 대한 증감은 최신 C++에서 사용이 권장되지 않습니다. 성능보다 더 중요한 원칙은 같은 객체를 여러 번 읽고 바꾸는 부수 효과를 한 표현식에 겹쳐 넣지 않는 것입니다.
산술식을 읽을 때는 결괏값뿐 아니라 결과 타입과 값이 바뀌는 시점을 함께 봐야 합니다.
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를 읽는다. |
|
|
이 절에서 lvalue는 x처럼 계속 존재하는 객체를 가리키는 표현식, prvalue는 x++가 내놓는 변경 전의 계산값으로 읽으면 됩니다. 모든 값 범주를 지금 외우기보다 “변수에 남은 값”과 “이번 식에서 받은 값”을 분리합니다. 대입·초기화가 끝난 다음 문장에서는 증감 효과도 반영되어 있습니다.
예시
다음은 앞의 부분 코드를 한 프로그램에서 비교한 것입니다. 두 증감을 별도 문장으로 두어 실행 순서를 명확히 합니다.
#include <iostream>
int main() {
int ticket{5};
int issued{ticket++};
int next{++ticket};
std::cout << issued << " " << next << " " << ticket << "\n";
}첫 증감은 issued에 이전 값 5를 주고 ticket을 6으로 바꿉니다. 다음 증감은 ticket을 7로 바꾼 뒤 그 값을 next에 줍니다. 출력은 5 7 7입니다. 한 식에 여러 증감을 넣어 같은 결과를 기대하지 않습니다.
직접 계산해 보기
실행 전에 두 가지를 적습니다. 위의 ticket++를 ++ticket로만 바꾸면 출력은 어떻게 달라질까요? 별도의 정수 a=-10, b=3에서 몫과 나머지를 구한 뒤 (a/b)*b+a%b를 계산하면 무엇이 나오나요?
값 변화와 음수 나머지 풀이
첫 질문은 issued가 6으로 바뀌어 6 7 7입니다. 둘째는 몫은 −3, 나머지는 −1이므로 (-3)*3+(-1)=-10입니다. 몫을 0 방향으로 버리는 규칙 때문에 음수에서도 나머지가 항상 양수라는 답은 맞지 않습니다. 제수가 0인 경우나 표현 불가능한 몫은 이 계산의 허용 입력에서 제외하며 직접 실행해 결과를 추측하지 않습니다.
산술 연산자 우선순위와 결합 규칙
하나의 표현식에 여러 연산자가 포함될 경우, 소스 코드가 어떤 구문 트리로 묶이는지는 연산자 우선순위(Operator Precedence)에 의해 결정됩니다.
우선순위가 높은 연산자가 주변 피연산자와 더 강하게 묶입니다. 이는 일반적인 피연산자의 런타임 평가 순서를 정하는 규칙과는 다릅니다.
예를 들어 수학에서 곱셈과 나눗셈이 덧셈과 뺄셈보다 우선순위가 높은 것과 같습니다.
| 우선순위 | 연산자 | 설명 |
|---|---|---|
| 높음 | x++, x-- | 후위 증감 |
++x, --x, +x, -x | 전위 증감과 단항 덧셈·뺄셈 | |
*, /, % | 곱셈, 나눗셈, 나머지 | |
+, - | 덧셈, 뺄셈 (이항) | |
| 낮음 | = | 대입 연산자 (후속 절에서 다룸) |
int result = 5 + 3 * 2; // 곱셈(*)이 덧셈(+)보다 우선순위가 높아 5 + (3 * 2)로 묶입니다.
// 5 + (3 * 2) = 5 + 6 = 11
std::cout << "5 + 3 * 2 = " << result << std::endl; // 출력: 11같은 우선순위를 가진 연산자들이 연속될 때는 결합 규칙(Associativity)에 따라 구문이 묶입니다.
표의 이항 산술 연산자 *, /, %, +, -는 왼쪽에서 오른쪽으로(Left-to-Right) 결합합니다. 전위 단항 연산자와 대입 연산자는 오른쪽에서 왼쪽으로 묶이므로, 결합 방향도 연산자 층마다 확인해야 합니다.
int result = 10 - 5 + 2; // 뺄셈과 덧셈은 우선순위가 같으므로 (10 - 5) + 2로 묶임
// (10 - 5) + 2 = 5 + 2 = 7
std::cout << "10 - 5 + 2 = " << result << std::endl; // 출력: 7괄호 () 사용:
기본 우선순위나 결합 규칙과 다른 묶음을 지정하려면 괄호 ()를 사용합니다.
괄호 안의 표현식은 바깥 연산자의 한 피연산자로 묶입니다.
int result = (5 + 3) * 2; // 괄호 안의 덧셈이 먼저 계산됩니다.
// (8) * 2 = 16
std::cout << "(5 + 3) * 2 = " << result << std::endl; // 출력: 16괄호는 연산 순서를 명확하게 하여 코드의 가독성을 높이는 데도 매우 유용합니다.
복잡한 표현식에서는 우선순위를 일일이 기억하기보다 괄호를 적절히 사용하여 의도를 명확히 하는 것이 좋습니다.