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

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

괄호 사용의 이점

명확한 연산 순서: 프로그래머의 의도를 명확하게 전달하여 코드를 읽는 사람이 쉽게 연산 순서를 파악할 수 있습니다.

오류 방지: 우선순위나 결합 규칙을 잘못 이해하여 발생할 수 있는 잠재적인 버그를 방지합니다.

코드 가독성 향상: 복잡한 표현식이라도 괄호를 통해 논리적인 그룹을 형성하여 코드를 더 이해하기 쉽게 만듭니다.

괄호 사용 예시
#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처럼 명시적 형 변환을 사용해야 했을 것입니다.

목차