연산자 우선순위
하나의 표현식 안에 여러 종류의 연산자가 함께 사용될 때 어떤 연산이 먼저 수행되는지 결정하는 매우 중요한 규칙인 연산자 우선순위(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 | |
| | 비트 OR | 왼쪽->오른쪽 | flags | 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_logic1
과 complex_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
처럼 명시적 형 변환을 사용해야 했을 것입니다.