기타 연산자
대입과 연쇄 대입에서 단계마다 일어나는 변환, 왼쪽을 한 번만 계산하는 복합 대입, 선택한 쪽만 계산하는 조건 연산자, 피연산자를 실행하지 않는 sizeof, 주소를 얻고 따라가는 &와 *, 왼쪽을 버리는 쉼표 연산자를 다룹니다.
산술과 비교, 논리, 비트 연산자 말고도 C++에는 자주 쓰는 연산자가 몇 가지 더 있습니다. 값을 저장하는 대입, 두 값 가운데 하나를 고르는 조건 연산자, 크기를 묻는 sizeof, 주소를 얻고 따라가는 &와 *, 두 식을 차례로 잇는 쉼표 연산자입니다.
이 연산자들은 짧지만, 어느 피연산자를 실제로 계산하는지와 결과가 무엇인지가 연산자마다 다릅니다. 이 글은 각 연산자가 무엇을 계산하고, 어떤 결과를 내며, 어떤 객체를 바꾸는지를 나누어 봅니다.
대입과 연쇄 대입
int age = 20;처럼 선언하면서 처음 값을 주는 것은 초기화이고, age = 21;처럼 이미 있는 객체의 값을 바꾸는 = 식이 대입입니다. 대입의 왼쪽은 값을 바꿀 수 있는 객체여야 하며, 오른쪽을 먼저 계산한 뒤 그 값을 왼쪽의 타입으로 바꾸어 저장합니다. 대입식의 결과는 값이 바뀐 왼쪽 객체 자신입니다.
대입은 오른쪽에서 왼쪽으로 결합하므로 x = y = z = 10은 x = (y = (z = 10))으로 묶입니다. 먼저 z에 10을 저장하고, 그 결과인 z의 값을 y에, 다시 y의 값을 x에 저장해 셋 모두 10이 됩니다.
대입 연쇄는 묶임, 평가, 변환, 저장을 단계별로 읽는다
3.8을 안쪽 int n에 저장하면 3, 안쪽 대입 결과를 바깥 double d에 저장하면 3.0이 되는 값 손실 경로이다.
ASSIGNMENT IS A STORE CONTRACT
대입 단계마다 왼쪽 타입으로 변환하고 저장한다
오른쪽 결합은 묶임을 정하고, 각 대입은 오른쪽 값 계산·타입 변환·저장·왼쪽 lvalue 결과를 차례로 만듭니다.
x = y = z = 10의 네 단계
구문 묶임
x = (y = (z = 10))· 오른쪽에서 왼쪽으로 그룹화합니다.z = 1010을z타입으로 변환해 저장하고, 식은z를 가리키는 lvalue가 됩니다.y = z직전 결과에서 값을 읽어
y타입으로 변환·저장하고, 결과는ylvalue입니다.x = y같은 계약으로
x를 갱신합니다. 일반적인 모든 하위식의 평가 방향을 뜻하지는 않습니다.
단계마다 변환
d = n = 3.8에서 먼저 n은 3, 이어 d는 3.0을 저장합니다.
결과는 왼쪽 lvalue
기본 제공 대입식은 왼쪽 피연산자의 타입이며 바로 그 객체를 가리킵니다.
복합 대입의 차이
E1 op= E2는 대응하는 대입과 같은 동작을 하되 위치 표현식 E1을 한 번만 평가합니다.
int age = 20;은 초기화이고, 이미 존재하는 객체에 쓰는 age = 21;이 대입 표현식입니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 먼저 int n에 대입할 때 3.8은 3으로 변환된다. 이어 n에 저장된 3을 d에 대입해 double 3.0이 된다. 바깥 double 타입이 안쪽 손실을 복원하지 않는다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 안쪽 대입 입력 3.8을 정수 n에 넣는다. |
|
|
| 안쪽 결과 안쪽 대입의 저장값과 식 결과를 확인한다. |
|
|
| 바깥 대입 그 3을 double d에 저장한다. |
|
|
대입 연쇄는 묶임, 평가, 변환, 저장을 단계별로 읽는다
3.8을 안쪽 int n에 저장하면 3, 안쪽 대입 결과를 바깥 double d에 저장하면 3.0이 되는 값 손실 경로이다.
ASSIGNMENT IS A STORE CONTRACT
대입 단계마다 왼쪽 타입으로 변환하고 저장한다
오른쪽 결합은 묶임을 정하고, 각 대입은 오른쪽 값 계산·타입 변환·저장·왼쪽 lvalue 결과를 차례로 만듭니다.
x = y = z = 10의 네 단계
구문 묶임
x = (y = (z = 10))· 오른쪽에서 왼쪽으로 그룹화합니다.z = 1010을z타입으로 변환해 저장하고, 식은z를 가리키는 lvalue가 됩니다.y = z직전 결과에서 값을 읽어
y타입으로 변환·저장하고, 결과는ylvalue입니다.x = y같은 계약으로
x를 갱신합니다. 일반적인 모든 하위식의 평가 방향을 뜻하지는 않습니다.
단계마다 변환
d = n = 3.8에서 먼저 n은 3, 이어 d는 3.0을 저장합니다.
결과는 왼쪽 lvalue
기본 제공 대입식은 왼쪽 피연산자의 타입이며 바로 그 객체를 가리킵니다.
복합 대입의 차이
E1 op= E2는 대응하는 대입과 같은 동작을 하되 위치 표현식 E1을 한 번만 평가합니다.
int age = 20;은 초기화이고, 이미 존재하는 객체에 쓰는 age = 21;이 대입 표현식입니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 먼저 int n에 대입할 때 3.8은 3으로 변환된다. 이어 n에 저장된 3을 d에 대입해 double 3.0이 된다. 바깥 double 타입이 안쪽 손실을 복원하지 않는다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 안쪽 대입 입력 3.8을 정수 n에 넣는다. |
|
|
| 안쪽 결과 안쪽 대입의 저장값과 식 결과를 확인한다. |
|
|
| 바깥 대입 그 3을 double d에 저장한다. |
|
|
그림처럼 타입이 다른 변수를 연쇄로 대입하면 단계마다 변환이 일어납니다. int n;과 double d;에서 d = n = 3.8;은 먼저 3.8을 int로 바꾸어 n에 3을 저장하고, 그 3을 다시 double로 바꾸어 d에 3.0을 저장합니다. 바깥의 d가 double이라고 해서 안쪽의 int에서 잃은 0.8이 돌아오지는 않습니다.
복합 대입
a += 3처럼 연산과 대입을 합친 것을 복합 대입(compound assignment)이라고 하며, +=, -=, *=, /=, %=와 비트 연산의 &=, |=, ^=, <<=, >>=가 있습니다. E1 += E2는 E1 = E1 + E2와 같은 일을 하지만, 왼쪽 E1을 한 번만 계산한다는 점이 다릅니다.
이 차이는 왼쪽에 함수 호출 같은 식이 있을 때 드러납니다. values[index()] += 3은 index()를 한 번만 불러 그 자리의 값에 3을 더하지만, values[index()] = values[index()] + 3은 index()를 두 번 부르며, 두 번의 결과가 다르면 다른 자리를 읽고 쓰게 됩니다. 복합 대입도 0으로 나누기나 범위를 넘는 계산처럼 그 연산이 가진 제한은 그대로 따릅니다.
조건 연산자
조건 연산자 조건 ? 값1 : 값2는 조건을 먼저 계산해, 참이면 값1을, 거짓이면 값2를 결과로 합니다. 피연산자가 셋이어서 삼항 연산자(ternary operator)라고도 부르며, 고른 쪽만 계산하고 다른 쪽은 계산하지 않습니다.
std::string label = score >= 60 ? "pass" : "fail";
int& selected = useLeft ? left : right;
selected = 0;첫 줄은 score가 75이면 "pass"를 고릅니다. 두 선택지가 모두 같은 타입의 변수라면 결과도 그 변수 자체가 되므로, 둘째 줄처럼 참조로 받아 고른 변수를 바꿀 수 있습니다. 두 선택지의 타입이 다르면 공통 타입으로 바뀐 값이 결과가 됩니다. 조건 연산자 안에 부수 효과가 있는 복잡한 식을 넣으면 읽기 어려우므로, 그럴 때는 if 문을 씁니다.
sizeof
sizeof(타입)이나 sizeof 식은 그 타입의 객체가 차지하는 바이트 수를 std::size_t 타입으로 돌려줍니다. 식을 피연산자로 주어도 그 식은 실행되지 않고 타입만 보므로, 이것을 평가하지 않는 피연산자(unevaluated operand)라고 합니다.
int value = 7;
std::size_t s = sizeof(value++);
int numbers[5]{};
std::size_t total = sizeof(numbers);
std::size_t count = sizeof(numbers) / sizeof(numbers[0]);sizeof(value++)는 int의 크기를 돌려줄 뿐 value++를 실행하지 않으므로, value는 그대로 7입니다. 배열에 sizeof를 쓰면 배열 전체의 바이트 수를 얻으므로 count는 5가 됩니다. 다만 함수의 매개변수로 받은 배열은 포인터로 바뀌어 있어 sizeof가 포인터의 크기를 돌려주므로, 배열의 원소 수는 C++17의 std::size(numbers)처럼 원소 수를 직접 구하는 도구를 쓰는 편이 안전합니다.
주소 연산자와 간접 참조 연산자
식에서 단항 &는 객체의 주소를 가리키는 포인터 값을 만들고, 단항 *는 포인터를 따라가 그 포인터가 가리키는 객체 자신을 나타냅니다. 포인터는 뒤의 장에서 자세히 다루며, 여기서는 두 연산자가 객체와 주소를 오가는 모습만 봅니다.
주소 취득과 간접 참조는 객체와 포인터를 왕복한다
value=100의 주소를 pointer에 저장하고 *pointer에 alias를 결합한 뒤 alias=200으로 같은 객체가 변한다.
OBJECT ↔ POINTER VALUE
주소를 얻어 저장하고 간접 참조로 같은 객체에 접근한다
&는 객체를 가리키는 포인터 값을 만들고, *는 포인터 값이 가리키는 대상의 lvalue를 만듭니다.
대상 객체
int value = 100;은 저장 공간과 수명을 가진 객체입니다.포인터 값 생성
&value는value를 가리키는int*prvalue를 만듭니다.포인터 객체 저장
int* pointer = &value;가 그 포인터 값을 저장합니다.대상 lvalue 복구
*pointer는value를 나타내므로 읽거나 대입할 수 있습니다.
- 실제 저장 객체
- 주소와 대상 사이의 변환
본문 예시 · 같은 value를 바꾼다
int value = 100;
int* pointer = &value;
int& alias = *pointer;
alias = 200;결과: value = 200 · pointer는 같은 객체를 계속 가리킴
간접 참조의 전제는 포인터가 허용된 대상을 가리키는 것입니다. null 또는 그 밖의 유효하지 않은 포인터를 값처럼 읽거나 쓰지 않습니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | value 객체에서 주소를 구해 pointer 객체에 저장하고 pointer를 간접 참조하면 원래 value 객체를 나타낸다. 네 노드/네 간선의 원래 가로 그림의 왕복 관계이며 실제 주소 숫자나 복사된 새 int를 뜻하지 않는다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 주소 연결 값이 100인 value의 주소를 pointer에 저장한다. |
|
|
| 별명 결합 *pointer에 alias 참조를 결합한다. |
|
|
| 참조 대입 alias에 200을 대입하면 같은 원본 value가 바뀐다. |
|
|
주소 취득과 간접 참조는 객체와 포인터를 왕복한다
value=100의 주소를 pointer에 저장하고 *pointer에 alias를 결합한 뒤 alias=200으로 같은 객체가 변한다.
OBJECT ↔ POINTER VALUE
주소를 얻어 저장하고 간접 참조로 같은 객체에 접근한다
&는 객체를 가리키는 포인터 값을 만들고, *는 포인터 값이 가리키는 대상의 lvalue를 만듭니다.
대상 객체
int value = 100;은 저장 공간과 수명을 가진 객체입니다.포인터 값 생성
&value는value를 가리키는int*prvalue를 만듭니다.포인터 객체 저장
int* pointer = &value;가 그 포인터 값을 저장합니다.대상 lvalue 복구
*pointer는value를 나타내므로 읽거나 대입할 수 있습니다.
- 실제 저장 객체
- 주소와 대상 사이의 변환
본문 예시 · 같은 value를 바꾼다
int value = 100;
int* pointer = &value;
int& alias = *pointer;
alias = 200;결과: value = 200 · pointer는 같은 객체를 계속 가리킴
간접 참조의 전제는 포인터가 허용된 대상을 가리키는 것입니다. null 또는 그 밖의 유효하지 않은 포인터를 값처럼 읽거나 쓰지 않습니다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | value 객체에서 주소를 구해 pointer 객체에 저장하고 pointer를 간접 참조하면 원래 value 객체를 나타낸다. 네 노드/네 간선의 원래 가로 그림의 왕복 관계이며 실제 주소 숫자나 복사된 새 int를 뜻하지 않는다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 주소 연결 값이 100인 value의 주소를 pointer에 저장한다. |
|
|
| 별명 결합 *pointer에 alias 참조를 결합한다. |
|
|
| 참조 대입 alias에 200을 대입하면 같은 원본 value가 바뀐다. |
|
|
#include <iostream>
int main() {
int value{100};
int saved{value};
int* pointer = &value;
int& alias = *pointer;
alias = 200;
std::cout << value << ' ' << *pointer << ' ' << saved << '\n';
}200 200 100그림처럼 &value로 value의 주소를 얻어 pointer에 저장하고, *pointer로 그 주소를 따라가면 다시 value 객체에 닿습니다. alias는 그 value에 붙인 다른 이름이어서, alias = 200은 새 객체를 만드는 것이 아니라 value에 200을 대입합니다. 그래서 value와 *pointer는 200이고, 앞에서 값을 복사해 둔 saved는 100 그대로입니다.
같은 기호라도 선언과 식에서 뜻이 다릅니다. int* pointer의 *는 포인터 타입을 만드는 선언의 일부이고, *pointer의 *는 간접 참조 연산자입니다. int& alias의 &도 참조를 선언하는 표시이고, &value의 &가 주소 연산자입니다. 널 포인터나 이미 사라진 객체를 가리키는 포인터에 *를 쓰면 정의되지 않은 동작이므로, 포인터가 올바른 객체를 가리키는지 먼저 확인해야 합니다.
쉼표 연산자
쉼표 연산자 왼쪽, 오른쪽은 왼쪽을 먼저 계산해 그 결과를 버리고, 오른쪽을 계산해 그것을 결과로 합니다. 아래 코드가 세 식을 괄호로 감싼 것은 쉼표 연산자의 우선순위가 모든 연산자 가운데 가장 낮기 때문입니다. 괄호가 없으면 선언 안의 쉼표는 변수 여러 개를 나누는 구분자로 읽혀 컴파일되지 않습니다.
#include <iostream>
int main() {
int x = 10;
int y = 20;
int result = (++x, y += 5, x + y);
std::cout << x << ' ' << y << ' ' << result << '\n';
}11 25 36괄호 안의 세 식이 왼쪽부터 차례로 실행되어 x는 11, y는 25가 되고, 마지막 식 x + y의 36이 결과가 됩니다. 쉼표 연산자는 for (int i = 0, j = 10; i < 5; ++i, --j)의 셋째 부분처럼 반복마다 두 변수를 함께 바꿀 때 주로 씁니다.
모든 쉼표가 이 연산자인 것은 아닙니다. 함수 인자 목록 f(a, b)와 선언 int i = 0, j = 10, 초기화 목록의 쉼표는 항목을 나누는 구분자이며, 왼쪽을 버리는 쉼표 연산자가 아닙니다. 이런 자리에서 쉼표 연산자를 쓰려면 괄호로 감싸 하나의 식임을 밝혀야 하며, 대부분은 문장을 나누어 쓰는 편이 읽기 쉽습니다.
연산자마다 나누어 읽기
EVALUATION → RESULT → PRECONDITION
평가 대상과 결과·전제를 연산자별로 구분한다
기호 이름보다 먼저 실제로 평가되는 피연산자, 만들어지는 결과, 안전하게 쓰기 위한 전제를 확인합니다.
E1 = E2 · 대입
평가 · E2가 E1보다 먼저입니다.
결과 · 변환 후 저장하며 결과는 E1 lvalue입니다.
c ? a : b · 조건
평가 · c 뒤 선택된 한 분기만 평가합니다.
결과 · 두 분기의 타입·값 범주 규칙으로 정합니다.
sizeof e · 크기
평가 · 표현식 e는 미평가 피연산자입니다.
결과 · 바이트 수인 std::size_t prvalue입니다.
&object · 주소
입력 · 객체나 함수를 나타내는 lvalue입니다.
결과 · 대상을 가리키는 포인터 prvalue입니다.
*pointer · 간접 참조
평가 · 포인터 값을 따라 대상을 찾습니다.
결과 · 대상 타입의 lvalue이며 유효한 대상이 전제입니다.
left, right · 콤마
평가 · 왼쪽을 끝낸 뒤 값을 버리고 오른쪽을 평가합니다.
결과 · 오른쪽의 타입·값·값 범주를 유지합니다.
이 비교는 기본 제공 연산자 계약입니다. 함수 인자·선언자·초기화 목록의 콤마는 구분자이며, 결합 방향만으로 일반 평가 순서를 추론하지 않습니다.
그림은 이 글의 연산자를 실제로 계산하는 피연산자와 결과, 쓰기 위한 조건으로 나누어 정리합니다. 대입은 오른쪽을 먼저 계산해 왼쪽 객체를 결과로 하고, 조건 연산자는 고른 쪽만 계산하며, sizeof는 피연산자를 실행하지 않습니다. 주소 연산자는 객체를, 간접 참조는 올바른 대상을 가리키는 포인터를 요구하고, 쉼표 연산자는 오른쪽의 결과만 남깁니다.
정리
- 대입은 오른쪽을 먼저 계산해 왼쪽의 타입으로 바꾸어 저장하며, 연쇄 대입은 오른쪽부터 묶이고 단계마다 변환이 일어납니다.
- 복합 대입은 연산과 대입을 합친 것으로, 왼쪽을 한 번만 계산한다는 점에서 풀어 쓴 대입과 다릅니다.
- 조건 연산자는 조건에 따라 두 값 가운데 하나를 고르며, 고른 쪽만 계산합니다.
sizeof는 타입이나 식의 바이트 수를 돌려주며, 피연산자의 식을 실행하지 않습니다.&는 객체의 주소를 얻고*는 포인터를 따라가 그 객체를 나타내며, 선언의*와&는 연산자가 아닙니다.- 쉼표 연산자는 왼쪽을 버리고 오른쪽을 결과로 하며, 함수 인자나 선언의 쉼표는 구분자입니다.
연습 문제
1. 가리키는 대상을 바꾸면
address_and_indirection.cpp에서 int* pointer = &value;를 int* pointer = &saved;로 바꾸면 무엇이 출력되나요?
해설 보기
alias가 saved를 가리키게 되므로 alias = 200은 saved를 바꿉니다. value는 100 그대로이고 *pointer와 saved는 200이 되어 100 200 200이 출력됩니다.
2. 실행되지 않는 식
int n = 7; 뒤에 std::size_t s = sizeof(n++);를 실행하면 n은 얼마인가요? s의 값은 어떤 플랫폼에서나 같은가요?
해설 보기
sizeof의 피연산자는 실행되지 않으므로 n은 그대로 7입니다. s는 int의 크기여서 많은 플랫폼에서 4이지만 표준은 이 값을 정하지 않으므로 플랫폼마다 다를 수 있습니다. 다만 n이 바뀌지 않는다는 점은 어디서나 같습니다.
3. 연쇄 대입의 변환
double d; int n;에서 n = d = 3.8;을 실행하면 d와 n은 각각 얼마인가요? 본문의 d = n = 3.8;과 무엇이 다른가요?
해설 보기
먼저 d = 3.8로 d가 3.8이 되고, 그 3.8을 int로 바꾸어 n에 3을 저장합니다. 본문의 d = n = 3.8;은 n을 먼저 거치므로 d가 3.0이 되었지만, 순서를 바꾸면 d는 3.8을 그대로 갖습니다. 연쇄 대입의 결과는 오른쪽에서 가까운 변수의 타입부터 차례로 정해집니다.
다음 글 연산자 우선순위에서는 지금까지 배운 연산자 전체의 우선순위와 결합 방향을 정리하고, 우선순위와 계산 순서가 어떻게 다른지를 다룹니다.