연산자 우선순위와 결합 방향

연산자 우선순위와 결합 방향 (Operator precedence)

표현식 하나에 연산자가 여럿 섞여 있으면, 컴파일러는 어느 연산을 먼저 묶어야 할지 정해야 해요. 이때 기준이 되는 게 **연산자 우선순위(operator precedence)**예요. 같은 줄에 적힌 연산자끼리는 우선순위가 같고, 우선순위가 같으면 **결합 방향(associativity)**이 다음 기준이 돼요. 아래 표는 C++ 연산자의 우선순위와 결합 방향을 위에서 아래로, 우선순위가 높은 순서대로 정리한 거예요. a, b, c는 피연산자(operand)를 뜻해요.

출처: cppreference

본문

우선순위 표

우선순위 연산자 설명 결합 방향
1 a::b 스코프 해석(Scope resolution) 왼쪽→오른쪽 →
2 a++ a-- 후위(접미사) 증가·감소
type (a) type {a} 함수 스타일 캐스트(Functional cast)
a() 함수 호출
a[] 첨자(Subscript)
a.b a->b 멤버 접근
3 ++a --a 전위 증가·감소 오른쪽→왼쪽 ←
+a -a 단항 플러스·마이너스
!a ~a 논리 NOT·비트 NOT
( type )a C 스타일 캐스트
*a 역참조(Indirection)
&a 주소 취하기(Address-of)
^^a 반사(reflection) 연산자(C++26부터)
sizeof 크기(Size-of)[^1]
co_await await-표현식(C++20)
newnew[] 동적 메모리 할당
deletedelete[] 동적 메모리 해제
4 a.*b a->*b 멤버 포인터(Pointer-to-member) 왼쪽→오른쪽 →
5 a * b a / b a % b 곱셈·나눗셈·나머지
6 a + b a - b 덧셈·뺄셈
7 a << b a >> b 비트 왼쪽·오른쪽 시프트
8 a <=> b 삼중 비교 연산자(C++20부터)
9 a < b a <= b a > b a >= b 관계 연산자<, <=, >, >= 각각에 대해
10 a == b a != b 동등 연산자==, != 각각에 대해
11 a & b 비트 AND
12 a ^ b 비트 XOR(배타적 논리합)
13 `a b` 비트 OR(포괄적 논리합)
14 a && b 논리 AND
15 `a b`
16 a ? b : c 삼항 조건부[^2] 오른쪽→왼쪽 ←
throw throw 연산자
co_yield yield-표현식(C++20)
a = b 직접 대입(C++ 클래스에 기본 제공)
a += b a -= b 합·차에 의한 복합 대입
a *= b a /= b a %= b 곱·몫·나머지에 의한 복합 대입
a <<= b a >>= b 비트 왼쪽·오른쪽 시프트에 의한 복합 대입
a &= b a ^= b `a = b` 비트 AND·XOR·OR에 의한 복합 대입
17 a, b 쉼표(Comma) 왼쪽→오른쪽 →

[^1]: sizeof의 피연산자는 C 스타일 타입 캐스트가 될 수 없어요. sizeof (int) * p라는 표현식은 모호함 없이 (sizeof(int)) * p로 해석되지, sizeof((int)*p)로 해석되지 않아요.

[^2]: 조건부 연산자의 가운데 표현식(?: 사이)은 괄호로 감싼 것처럼 파싱돼요. 즉 ?:에 대한 그 표현식의 우선순위는 무시돼요.

우선순위가 어떻게 적용되나

표의 어떤 행에 우선순위와 함께 적힌 연산자는, 그보다 아래 행에 있는 더 낮은 우선순위의 연산자보다 자기 피연산자에 더 꽉 붙어(마치 괄호로 감싼 것처럼) 바인딩돼요. 예를 들어 std::cout << a & b*p++는 각각 (std::cout << a) & b*(p++)로 파싱되지, std::cout << (a & b)(*p)++로 파싱되지 않아요.

우선순위가 같은 연산자는 결합 방향에 따라 피연산자에 바인딩돼요. 예를 들어 a = b = c는 대입의 오른쪽→왼쪽 결합 때문에 a = (b = c)로 파싱되지 (a = b) = c로 파싱되지 않아요. 반면 a + b - c는 덧셈·뺄셈의 왼쪽→오른쪽 결합 때문에 (a + b) - c로 파싱돼요.

결합 방향 표기는 단항 연산자에는 사실상 불필요하지만 완전성을 위해 보여 주는 거예요. 전위 단항 연산자는 항상 오른쪽→왼쪽(delete ++*pdelete(++(*p))), 후위 단항 연산자는 항상 왼쪽→오른쪽(a[1][2]++((a[1])[2])++)으로 결합해요. 멤버 접근 연산자는 후위 단항 연산자와 묶여 있지만 결합 방향이 의미 있다는 점만 기억하면 돼요. a.b++a.(b++)이 아니라 (a.b)++로 파싱돼요.

참고로 연산자 우선순위는 연산자 오버로딩에 영향받지 않아요. 예를 들어 std::cout << a ? b : c;는 산술 왼쪽 시프트의 우선순위가 조건부 연산자보다 높기 때문에 (std::cout << a) ? b : c;로 파싱돼요.

주의할 점(Notes)

우선순위와 결합 방향은 컴파일 타임 개념이고, 런타임 개념인 평가 순서(order of evaluation)와는 별개예요.

표준 자체는 우선순위 단계를 직접 명시하지 않아요. 그것들은 문법(grammar)에서 유도된 거예요.

const_cast, static_cast, dynamic_cast, reinterpret_cast, typeid, sizeof..., noexcept, alignof는 절대 모호해지지 않아서 우선순위 표에 포함되지 않아요.

일부 연산자에는 대체 표기(alternate spelling)가 있어요. 예를 들어 &&and, ||or, !not으로 쓸 수 있죠.

C에서는 삼항 조건부 연산자가 대입 연산자보다 우선순위가 높아요. 그래서 C++에서 e = ((a < d) ? (a++) : (a = d))로 파싱되는 e = a < d ? a++ : a = d 같은 표현식은, C의 문법·의미상 제약 때문에 C에서는 컴파일되지 않아요. 자세한 내용은 C 페이지를 참고해요.

더 알아보기