본문으로 건너뛰기

안동민 개발노트

본문 시작

산술 연산자

산술 연산자의 피연산자와 결과 타입을 읽고 정수 나눗셈·나머지·증감 연산의 값 변경 시점을 구분합니다.

데이터들을 실제로 움직이고 계산하는 방법에 대해 배울 차례입니다.

그 중심에는 바로 연산자(Operators)가 있습니다.

연산자는 특정 연산을 수행하도록 컴파일러에 지시하는 기호입니다.

이 장에서는 사칙연산을 비롯하여 숫자를 계산하고 조작하는 데 사용되는 산술 연산자(Arithmetic Operators)를 다룹니다.

먼저 산술 연산자를 구문 묶음, 연산 전제, 결과 타입, 값 변경 시점이라는 네 축으로 나눠 봅니다.


연산자와 표현식의 개념

먼저 연산자, 피연산자, 표현식이 어떻게 연결되는지부터 잡습니다.

본격적으로 산술 연산자를 살펴보기 전에, 연산자표현식이라는 개념을 명확히 하는 것이 중요합니다.

  • 연산자 (Operator): 특정 연산을 수행하도록 컴파일러에 지시하는 기호입니다. (예: +, -, *, /, %)
  • 피연산자 (Operand): 연산의 대상이 되는 값이나 변수입니다. (예: a + b에서 ab)
  • 표현식 (Expression): 연산자와 피연산자 등을 조합해 계산이나 동작을 기술하는 코드 조각입니다. 표현식의 타입과 값 범주를 판정할 수 있고, 종류에 따라 값이나 부수 효과를 만듭니다. 이 장의 a + b, score * 2, (x + y) / 2 같은 산술 표현식은 값을 만듭니다.
연산자와 표현식 예시
int result = 10 + 20; // '10', '20'은 피연산자, '+'는 연산자, '10 + 20'은 표현식, 'result'는 변수

산술식은 기호 하나만 보는 것이 아니라 피연산자 타입, 우선순위, 최종 결과 타입을 함께 읽어야 합니다.

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

Expression reading contract

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

  1. 구문을 묶습니다

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

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

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

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

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

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

    정수 나눗셈은 몫을 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)을 거쳐 두 피연산자의 공통 타입을 정합니다. 아래 intdouble 조합에서는 intdouble로 변환된 뒤 결과 타입도 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

    • 내장 전위 증가 ++xx += 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++에서 사용이 권장되지 않습니다. 성능보다 더 중요한 원칙은 같은 객체를 여러 번 읽고 바꾸는 부수 효과를 한 표현식에 겹치지 않는 것입니다.

산술식을 읽을 때는 결과값뿐 아니라 결과 타입과 값이 바뀌는 시점을 함께 봐야 합니다.

초깃값이 5일 때 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
내장 증감 연산의 대상은 수정 가능한 lvalue여야 합니다. 이전 값이 필요하지 않다면 의도가 드러나는 형식을 고르고, 여러 부수 효과를 한 식에 겹치지 않습니다.

산술 연산자 우선순위와 결합 규칙

하나의 표현식에 여러 연산자가 포함될 경우, 소스 코드가 어떤 구문 트리로 묶이는지는 연산자 우선순위(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

괄호는 연산 순서를 명확하게 하여 코드의 가독성을 높이는 데도 매우 유용합니다.

복잡한 표현식에서는 우선순위를 일일이 기억하기보다 괄호를 적절히 사용하여 의도를 명확히 하는 것이 좋습니다.