연산자 우선순위
여러 연산자가 섞인 표현식의 구문 묶임을 정하고, 실제 평가 순서와 부분식별 타입 변환을 별도로 확인합니다.
하나의 표현식에 여러 연산자가 함께 쓰이면 먼저 어떤 피연산자가 어느 연산자에 속하는지 정해야 합니다.
이 구문 묶임을 정하는 규칙이 **연산자 우선순위(Operator Precedence)**와 **결합 방향(Associativity)**입니다. 그러나 두 규칙은 런타임의 피연산자 평가 순서를 일반적으로 정하지 않습니다. 평가와 부수 효과의 순서는 각 연산자에 정의된 sequencing 규칙을 따로 확인해야 합니다.
GROUPING IS NOT EVALUATION ORDER
우선순위와 결합 방향은 구문 트리를 만들고, sequencing은 각 연산자의 별도 계약이 정합니다.
묶임을 확정
명시적 괄호, 우선순위, 같은 단계의 결합 방향으로 어느 피연산자가 어느 연산자에 속하는지 적습니다.
연산자 계약 적용
각 노드에서 허용 타입, 변환, 전제 조건, 결과 타입·값 범주와 부수 효과를 확인합니다.
평가 순서를 별도 확인
&&,||,?:, 대입, 콤마처럼 순서를 정한 경우만 믿고, 보장이 없으면 부수 효과를 분리합니다.
5 + 3 * 2
묶임 5 + (3 * 2)
결과 안쪽 곱의 값 6을 사용해 11을 만듭니다.
10 - 5 + 2
묶임 (10 - 5) + 2
경계 왼쪽 결합은 일반적인 피연산자 실행 방향이 아닙니다.
a = b = 10
묶임 a = (b = 10)
sequencing 기본 제공 대입은 오른쪽 피연산자를 왼쪽보다 먼저 평가합니다.
f() + g()
묶임 두 호출 결과가 +의 피연산자입니다.
sequencing 어느 호출이 먼저인지 우선순위로 정할 수 없습니다.
괄호는 묶임을 명확하게 하지만 보통 새로운 sequencing을 만들지 않습니다. 순서가 중요한 부수 효과는 중간 변수와 별도 문장으로 표현합니다.
연산자 우선순위란?
우선순위가 높은 연산자는 주변 피연산자를 더 강하게 묶습니다.
int result = 5 + 3 * 2;곱셈 연산자 *가 덧셈 연산자 +보다 높은 단계에 있으므로 이 식은 다음처럼 해석됩니다.
int result = 5 + (3 * 2); // 113 * 2의 값이 바깥쪽 +의 오른쪽 피연산자가 되어 결과는 11입니다. 반면 (5 + 3) * 2처럼 괄호로 묶음을 바꾸면 결과는 16입니다.
여기서 “곱셈이 먼저 묶인다”와 “모든 하위 표현식이 높은 우선순위 순서로 실행된다”는 다른 주장입니다. 특히 f() + g()에서 +의 두 피연산자 호출 순서는 우선순위로 정할 수 없습니다.
결합 방향이란?
결합 방향은 같은 우선순위 단계의 연산자가 연속될 때 구문이 어느 쪽으로 묶이는지 정합니다.
int result = 10 - 5 + 2;
// (10 - 5) + 2 -> 7이항 +와 -는 같은 단계에서 왼쪽으로 묶이므로 결과는 7입니다. 이는 일반적인 피연산자 평가 방향을 “항상 왼쪽부터”로 정한다는 뜻이 아닙니다.
대입 연산자는 오른쪽으로 묶입니다.
int a = 0;
int b = 0;
a = b = 10;
// a = (b = 10)기본 제공 대입에서는 오른쪽 피연산자가 왼쪽 피연산자보다 먼저 평가되고, 두 피연산자의 값 계산 뒤 저장이 일어납니다. 따라서 안쪽 b = 10의 결과가 바깥쪽 대입에 사용되어 두 객체가 모두 10이 됩니다.
주요 우선순위 단계
C++ 표준은 별도의 숫자 표 대신 표현식 문법으로 묶임을 정의합니다. 다음 표는 이 절에서 자주 쓰는 기본 제공 연산자를 높은 단계부터 요약한 읽기 도구입니다.
| 상대 단계 | 주요 형태 | 기본 묶임 | 읽을 때 확인할 점 |
|---|---|---|---|
| 높음 | a(), a[], a.m, p->m, x++ | 왼쪽 | 호출·첨자·멤버 접근·후위 증감 |
++x, --x, +x, -x, !x, ~x, *p, &x, sizeof x | 오른쪽 | 전위·단항 표현식 | |
p->*m, a.*m | 왼쪽 | 멤버 포인터 | |
*, /, % | 왼쪽 | usual arithmetic conversions, /·%의 0 제수 금지 | |
+, - | 왼쪽 | 산술 또는 포인터 연산 계약 | |
<<, >> | 왼쪽 | 정수 승격, 결과 타입, 유효한 이동 수 | |
<=> | 왼쪽 | 세 방향 비교 | |
<, <=, >, >= | 왼쪽 | 연쇄 비교는 수학식과 다르게 묶임 | |
==, != | 왼쪽 | 관계 연산보다 낮은 단계 | |
& | 왼쪽 | 비트 AND | |
^ | 왼쪽 | 비트 XOR | |
| | 왼쪽 | 비트 OR | |
&& | 왼쪽 | 왼쪽 우선 평가와 false 단락 | |
|| | 왼쪽 | 왼쪽 우선 평가와 true 단락 | |
?:, =, +=, -=, *=, /= 등 | 오른쪽 | 조건 분기와 대입에는 별도 sequencing 규칙이 있음 | |
| 낮음 | , | 왼쪽 | 기본 제공 콤마 연산자는 왼쪽을 먼저 평가하고 버림 |
함수 인수, 초기화 목록, 템플릿 인수 등에 쓰인 쉼표는 구분자이며 기본 제공 콤마 연산자가 아닙니다. 오버로드된 연산자는 구문상의 우선순위와 묶임은 유지하지만, 피연산자 타입과 의미에는 함수 호출·오버로드 해석 규칙이 관여합니다.
괄호는 묶음을 명시한다
괄호로 만든 parenthesized expression은 기본 우선순위가 만들 묶음을 바꿉니다. 괄호 자체를 “모든 연산자보다 높은 연산자”로 보기보다는, 원하는 부분식을 문법적으로 하나로 묶는 장치로 이해하는 편이 정확합니다.
int a = 10;
int b = 5;
int c = 2;
int by_precedence = a + b * c; // a + (b * c) -> 20
int by_intent = (a + b) * c; // (a + b) * c -> 30
bool enabled = true;
bool ready = false;
bool valid = true;
bool default_group = enabled || ready && valid;
// enabled || (ready && valid)
bool explicit_group = (enabled || ready) && valid;괄호는 묶임을 분명하게 만들지만, 보통 연산자의 피연산자 사이에 없던 sequencing까지 추가하지는 않습니다. 예를 들어 (f()) + (g())의 괄호는 두 호출의 평가 순서를 고정하지 않습니다. 부수 효과의 순서가 중요하다면 별도 문장과 중간 변수로 표현합니다.
묶인 부분식마다 타입을 결정한다
산술 변환은 단순히 “더 큰 타입으로 바뀐다”는 규칙이 아닙니다. 이 예에서 산술 타입을 받는 기본 제공 *, /, +는 각 노드의 피연산자에 usual arithmetic conversions를 적용하고 그 공통 타입으로 연산합니다. 필요한 정수 승격도 이 변환 과정에 포함됩니다. 시프트처럼 다른 변환·결과 타입 규칙을 가진 연산자는 해당 계약을 따로 확인해야 합니다.
double d_val = 10.5;
int i_val = 4;
double result_div = d_val / i_val;
// double / double -> 2.625
double result_complex = 2 * d_val + i_val / 3;
// (2 * d_val) + (i_val / 3)
// 21.0(double) + 1(int) -> 22.0(double)2 * d_val에서는 2가 double 쪽 산술 변환을 거쳐 21.0을 만듭니다. 별개의 오른쪽 부분식 i_val / 3은 두 피연산자가 int이므로 소수부를 버린 정수 몫 1을 만듭니다. 바깥쪽 +가 그 1을 double로 변환해 최종 값 22.0을 만듭니다.
두 곱셈·나눗셈 부분식은 모두 바깥쪽 덧셈의 피연산자이지만, +가 왼쪽 부분식을 오른쪽 부분식보다 먼저 평가하도록 보장하지는 않습니다. 이 예에는 부수 효과가 없으므로 어느 쪽을 먼저 평가해도 같은 값이 나옵니다.
오른쪽 나눗셈 자체를 실수 나눗셈으로 바꾸려면 변환을 그 부분식 안에 둡니다.
double precise = 2 * d_val + static_cast<double>(i_val) / 3;
// 21.0 + 1.333... -> 22.333...TYPE FLOW FOLLOWS THE PARSE TREE
2 * d_val + i_val / 3은 두 부분식에서 각각 변환·연산한 뒤 바깥쪽 +에서 다시 공통 타입을 정합니다.
구문 묶임
(2 * d_val) + (i_val / 3)으로 두 자식 부분식을 나눕니다.왼쪽 곱셈
2가double로 변환되어21.0을 만듭니다.오른쪽 나눗셈
int / int의 결과는 소수부를 버린1입니다.바깥 덧셈
1이double로 변환되어 최종22.0을 만듭니다.
현재 변환 위치
i_val / 3이 먼저 int 결과 1을 만들고, 바깥 덧셈에서 그 결과를 double로 변환합니다.
나눗셈 전에 변환
static_cast<double>(i_val) / 3은 나눗셈 자체를 실수 연산으로 만들어 약 1.333을 남깁니다.
두 자식 부분식의 값은 바깥 덧셈 결과보다 먼저 필요하지만, 기본 제공 +는 왼쪽 자식을 오른쪽 자식보다 먼저 평가한다고 보장하지 않습니다.
복합 표현식은 다음 순서로 검토하면 안전합니다.
묶임: 명시적 괄호와 연산자 문법으로 부분식 구조를 적습니다.
연산자 계약: 각 부분식의 허용 타입, 전제 조건, 변환, 결과 타입과 값 범주를 확인합니다.
sequencing: 부수 효과가 있다면 실제 평가 순서가 보장되는지 별도로 확인합니다.
의도 표현: 한눈에 확신하기 어렵다면 괄호, 중간 변수, 별도 문장으로 다시 씁니다.
우선순위 표는 구문을 읽는 도구입니다. 정확한 코드는 묶임, 연산자별 의미, 실제 평가 순서를 서로 바꾸어 말하지 않습니다.