산술 연산자
산술 연산자의 피연산자와 결과 타입을 읽고 정수 나눗셈·나머지·증감 연산의 값 변경 시점을 구분합니다.
데이터들을 실제로 움직이고 계산하는 방법에 대해 배울 차례입니다.
그 중심에는 바로 연산자(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++에서 사용이 권장되지 않습니다. 성능보다 더 중요한 원칙은 같은 객체를 여러 번 읽고 바꾸는 부수 효과를 한 표현식에 겹치지 않는 것입니다.
산술식을 읽을 때는 결과값뿐 아니라 결과 타입과 값이 바뀌는 시점을 함께 봐야 합니다.
Prefix vs postfix
둘 다 저장된 값을 6으로 만들지만, 대입에 사용되는 표현식 결과는 다릅니다.
- 전위
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
산술 연산자 우선순위와 결합 규칙
하나의 표현식에 여러 연산자가 포함될 경우, 소스 코드가 어떤 구문 트리로 묶이는지는 연산자 우선순위(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괄호는 연산 순서를 명확하게 하여 코드의 가독성을 높이는 데도 매우 유용합니다.
복잡한 표현식에서는 우선순위를 일일이 기억하기보다 괄호를 적절히 사용하여 의도를 명확히 하는 것이 좋습니다.