icon
3장 : 연산자와 표현식

연산자 우선순위

하나의 표현식 안에 여러 종류의 연산자가 함께 사용될 때 어떤 연산이 먼저 수행되는지 결정하는 매우 중요한 규칙인 연산자 우선순위(Operator Precedence)결합 규칙(Associativity) 에 대해 심층적으로 알아보겠습니다.

이 규칙들을 정확히 이해하지 못하면, 예상치 못한 계산 결과로 인해 프로그램에 논리적 오류가 발생할 수 있습니다.

반대로 이 규칙들을 잘 활용하면 코드를 간결하면서도 정확하게 작성할 수 있습니다.


연산자 우선순위란?

연산자 우선순위는 하나의 표현식 내에서 두 개 이상의 연산자가 함께 사용될 때 어떤 연산자가 다른 연산자보다 먼저 평가되어야 하는지를 결정하는 규칙입니다.

수학에서 곱셈과 나눗셈이 덧셈과 뺄셈보다 먼저 수행되는 것과 동일한 개념입니다.

연산자 우선순위 예시
int result = 5 + 3 * 2;

이 표현식에서 +* 연산자가 함께 있습니다.

만약 우선순위가 없다면, (5 + 3) * 2 = 16이 될 수도 있고 5 + (3 * 2) = 11이 될 수도 있습니다.

C++의 연산자 우선순위 규칙에 따르면 *+보다 우선순위가 높으므로, 3 * 2가 먼저 계산되어 6이 되고, 그 다음 5 + 6이 계산되어 최종 결과는 11이 됩니다.


연산자 결합 규칙이란?

연산자 결합 규칙(Associativity) 은 동일한 우선순위를 가진 두 개 이상의 연산자가 하나의 표현식에 있을 때, 연산이 왼쪽에서 오른쪽으로 진행될지, 아니면 오른쪽에서 왼쪽으로 진행될지를 결정하는 규칙입니다.

연산자 결합 규칙 예시
int result = 10 - 5 + 2;

여기서 -+는 동일한 우선순위를 가집니다.

이들의 결합 규칙은 왼쪽에서 오른쪽입니다. 따라서 (10 - 5)가 먼저 계산되어 5가 되고, 그 다음 5 + 2가 계산되어 최종 결과는 7이 됩니다.

만약 오른쪽에서 왼쪽이었다면 10 - (5 + 2) = 10 - 7 = 3이 되었을 것입니다.

대부분의 이항(binary) 연산자(산술, 관계, 비트 연산자 등)는 왼쪽에서 오른쪽으로 결합합니다.

하지만 대입 연산자 (=, +=, -= 등)와 단항 연산자 (++, --, !, ~, &, * 등)는 오른쪽에서 왼쪽으로 결합합니다.


C++ 연산자 우선순위 및 결합 규칙 요약

다음 표는 주요 C++ 연산자들의 우선순위와 결합 규칙을 높은 우선순위부터 낮은 우선순위 순으로 요약한 것입니다.

모든 연산자를 포함하지는 않지만, 가장 자주 사용되는 것들을 중심으로 구성했습니다.

우선순위연산자설명결합 규칙예시
높음::스코프 결정왼쪽->오른쪽std::cout
()함수 호출, 그룹화왼쪽->오른쪽myFunction(arg), (a + b)
[]배열 요소 접근왼쪽->오른쪽myArray[index]
.멤버 접근 (객체)왼쪽->오른쪽object.member
->멤버 접근 (포인터)왼쪽->오른쪽pointer->member
++ (후위), -- (후위)후위 증감왼쪽->오른쪽x++, y--
++ (전위), -- (전위)전위 증감오른쪽->왼쪽++x, --y
+ (단항), - (단항)단항 덧셈, 뺄셈오른쪽->왼쪽+5, -value
!논리 NOT오른쪽->왼쪽!isTrue
~비트 NOT오른쪽->왼쪽~flags
(type)C-스타일 형 변환오른쪽->왼쪽(int)3.14
* (포인터)간접 참조 (역참조)오른쪽->왼쪽*ptr
& (포인터)주소 연산자오른쪽->왼쪽&variable
sizeof크기 연산자오른쪽->왼쪽sizeof(int)
new, delete동적 메모리 할당/해제오른쪽->왼쪽new int, delete ptr
*, /, %곱셈, 나눗셈, 나머지왼쪽->오른쪽a * b, c / d, x % y
+, -덧셈, 뺄셈왼쪽->오른쪽a + b, c - d
<<, >>비트 시프트왼쪽->오른쪽val << 2, val >> 1
<, <=, >, >=관계 (비교)왼쪽->오른쪽a < b, x >= y
==, !=동등성 (비교)왼쪽->오른쪽a == b, x != y
&비트 AND왼쪽->오른쪽flags & mask
^비트 XOR왼쪽->오른쪽val ^ key
&#124;비트 OR왼쪽->오른쪽flags &#124; option
&&논리 AND왼쪽->오른쪽condition1 && condition2
||논리 OR왼쪽->오른쪽condition1 || condition2
? :조건 (삼항)오른쪽->왼쪽condition ? val1 : val2
=대입오른쪽->왼쪽x = 10
+=, -=, *=복합 대입오른쪽->왼쪽x += 5
낮음,콤마왼쪽->오른쪽expr1, expr2

참고: 위 표는 C++의 모든 연산자를 포함하지 않으며, 주로 다루었던 연산자들을 중심으로 구성했습니다.

실제 C++ 표준에는 훨씬 더 많은 연산자와 복잡한 우선순위 규칙이 있습니다.


괄호 ()의 중요성

연산자 우선순위와 결합 규칙을 모두 외워서 적용하는 것은 매우 번거롭고 실수할 확률이 높습니다.

C++에서는 이러한 혼동을 피하고 코드의 가독성을 극대화하기 위해 괄호 () 를 사용하는 것을 강력히 권장합니다.

괄호는 모든 연산자보다 가장 높은 우선순위를 가집니다. 괄호 안에 있는 표현식은 항상 가장 먼저 평가됩니다.

괄호 사용의 이점

  1. 명확한 연산 순서: 프로그래머의 의도를 명확하게 전달하여 코드를 읽는 사람이 쉽게 연산 순서를 파악할 수 있습니다.
  2. 오류 방지: 우선순위나 결합 규칙을 잘못 이해하여 발생할 수 있는 잠재적인 버그를 방지합니다.
  3. 코드 가독성 향상: 복잡한 표현식이라도 괄호를 통해 논리적인 그룹을 형성하여 코드를 더 이해하기 쉽게 만듭니다.
괄호 사용 예시
#include <iostream>

int main() {
    int a = 10, b = 5, c = 2;

    // 괄호 없이 우선순위에 따라
    int result1 = a + b * c;     // a + (b * c) -> 10 + (5 * 2) = 10 + 10 = 20
    std::cout << "Result1 (a + b * c): " << result1 << std::endl; // 출력: 20

    // 괄호를 사용하여 덧셈을 먼저
    int result2 = (a + b) * c;   // (a + b) * c -> (10 + 5) * 2 = 15 * 2 = 30
    std::cout << "Result2 ((a + b) * c): " << result2 << std::endl; // 출력: 30

    bool condition1 = true, condition2 = false, condition3 = true;

    // 논리 연산자 우선순위 (AND가 OR보다 높음)
    bool complex_logic1 = condition1 || condition2 && condition3;
    // condition1 || (condition2 && condition3)
    // true || (false && true)
    // true || false -> true
    std::cout << "Complex Logic 1 (true || false && true): " << std::boolalpha << complex_logic1 << std::endl; // 출력: true

    // 괄호를 사용하여 OR를 먼저
    bool complex_logic2 = (condition1 || condition2) && condition3;
    // (condition1 || condition2) && condition3
    // (true || false) && true
    // true && true -> true
    std::cout << "Complex Logic 2 ((true || false) && true): " << std::boolalpha << complex_logic2 << std::endl; // 출력: true

    // 비트 연산자와 논리 연산자 혼합
    int val = 0b0101; // 5
    int mask = 0b0010; // 2
    bool check = (val & mask) && (val > 0);
    // (val & mask) -> (0101 & 0010) = 0000 (십진수 0)
    // 0 (false) && (5 > 0 = true) -> false
    std::cout << "Bitwise and Logical Check: " << std::boolalpha << check << std::endl; // 출력: false

    return 0;
}

위의 complex_logic1complex_logic2 예시에서는 결과가 우연히 같았지만, 항상 그런 것은 아닙니다.

괄호를 사용하여 의도를 명확히 하는 습관을 들이는 것이 중요합니다.


연산자 우선순위와 타입 변환

연산자 우선순위와 결합 규칙은 형 변환(Type Conversion)이 발생하는 순서에도 영향을 미칠 수 있습니다.

특히 산술 연산 시 피연산자의 타입이 다를 경우, 더 큰 타입으로의 암시적 형 변환이 연산자 우선순위에 따라 특정 시점에 이루어집니다.

연산자 우선순위와 타입 변환 예시
double d_val = 10.5;
int i_val = 4;

double result_div = d_val / i_val; // i_val이 double로 변환 후 나눗셈
// 10.5 / 4.0 = 2.625

double result_complex = 2 * d_val + i_val / 3;
// 1. i_val / 3  -> 4 / 3 = 1 (정수 나눗셈 먼저)
// 2. 2 * d_val  -> 2.0 * 10.5 = 21.0 (double로 변환 후 곱셈)
// 3. 21.0 + 1.0 (정수 1이 double로 변환) = 22.0
std::cout << "Result complex: " << result_complex << std::endl; // 출력: 22

이 예시에서 i_val / 3은 정수 나눗셈으로 먼저 수행되어 1이 되고, 이 1이 나중에 double 타입으로 변환되어 21.0과 더해집니다.

만약 i_val을 먼저 double로 변환하고 싶었다면 static_cast<double>(i_val) / 3처럼 명시적 형 변환을 사용해야 했을 것입니다.