본문 바로가기

기타 연산자

대입과 연쇄 대입에서 단계마다 일어나는 변환, 왼쪽을 한 번만 계산하는 복합 대입, 선택한 쪽만 계산하는 조건 연산자, 피연산자를 실행하지 않는 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이 되는 값 손실 경로이다.

기본 제공 대입과 복합 대입에서 오른쪽 결합, 오른쪽 우선 평가, 왼쪽 타입 변환, lvalue 결과와 왼쪽 피연산자 한 번 평가 계약

ASSIGNMENT IS A STORE CONTRACT

대입 단계마다 왼쪽 타입으로 변환하고 저장한다

오른쪽 결합은 묶임을 정하고, 각 대입은 오른쪽 값 계산·타입 변환·저장·왼쪽 lvalue 결과를 차례로 만듭니다.

d = n = 3.8의 두 저장 경계먼저intn에대입할때3.8은3으로변환된다. 이어n에저장된3을d에대입해double3.0이된다. 바깥double타입이안쪽손실을복원하지않는다.
오른쪽 입력3.8 · double
연결 관계오른쪽 입력 · 3.8 · double → n에 대입 · 3 · int
n에 대입3 · int
연결 관계n에 대입 · 3 · int → d에 대입 · 3.0 · double
d에 대입3.0 · double

x = y = z = 10의 네 단계

  1. 구문 묶임

    x = (y = (z = 10)) · 오른쪽에서 왼쪽으로 그룹화합니다.

  2. z = 10

    10을 z 타입으로 변환해 저장하고, 식은 z를 가리키는 lvalue가 됩니다.

  3. y = z

    직전 결과에서 값을 읽어 y 타입으로 변환·저장하고, 결과는 y lvalue입니다.

  4. 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.8
  • 대상: int n
  • int 대입은 소수부를 보존하지 않는다.
안쪽 결과

안쪽 대입의 저장값과 식 결과를 확인한다.

  • n: 3
  • 안쪽 결과: 3
  • 바깥 대입이 받는 값은 원래 3.8이 아니라 변환된 3이다.
바깥 대입

그 3을 double d에 저장한다.

  • n: 3
  • d: 3.0
  • double 변환은 이미 버린 소수부를 복원하지 않는다.

대입 연쇄는 묶임, 평가, 변환, 저장을 단계별로 읽는다

3.8을 안쪽 int n에 저장하면 3, 안쪽 대입 결과를 바깥 double d에 저장하면 3.0이 되는 값 손실 경로이다.

기본 제공 대입과 복합 대입에서 오른쪽 결합, 오른쪽 우선 평가, 왼쪽 타입 변환, lvalue 결과와 왼쪽 피연산자 한 번 평가 계약

ASSIGNMENT IS A STORE CONTRACT

대입 단계마다 왼쪽 타입으로 변환하고 저장한다

오른쪽 결합은 묶임을 정하고, 각 대입은 오른쪽 값 계산·타입 변환·저장·왼쪽 lvalue 결과를 차례로 만듭니다.

d = n = 3.8의 두 저장 경계먼저intn에대입할때3.8은3으로변환된다. 이어n에저장된3을d에대입해double3.0이된다. 바깥double타입이안쪽손실을복원하지않는다.
오른쪽 입력3.8 · double
연결 관계오른쪽 입력 · 3.8 · double → n에 대입 · 3 · int
n에 대입3 · int
연결 관계n에 대입 · 3 · int → d에 대입 · 3.0 · double
d에 대입3.0 · double

x = y = z = 10의 네 단계

  1. 구문 묶임

    x = (y = (z = 10)) · 오른쪽에서 왼쪽으로 그룹화합니다.

  2. z = 10

    10을 z 타입으로 변환해 저장하고, 식은 z를 가리키는 lvalue가 됩니다.

  3. y = z

    직전 결과에서 값을 읽어 y 타입으로 변환·저장하고, 결과는 y lvalue입니다.

  4. 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.8
  • 대상: int n
  • int 대입은 소수부를 보존하지 않는다.
안쪽 결과

안쪽 대입의 저장값과 식 결과를 확인한다.

  • n: 3
  • 안쪽 결과: 3
  • 바깥 대입이 받는 값은 원래 3.8이 아니라 변환된 3이다.
바깥 대입

그 3을 double d에 저장한다.

  • n: 3
  • d: 3.0
  • double 변환은 이미 버린 소수부를 복원하지 않는다.

그림처럼 타입이 다른 변수를 연쇄로 대입하면 단계마다 변환이 일어납니다. 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으로 같은 객체가 변한다.

value 객체에서 주소 연산자로 pointer 값을 만들고 포인터 객체에 저장한 뒤 간접 참조 연산자로 value 객체를 나타내는 lvalue를 얻어 읽거나 쓰는 순환 구조

OBJECT ↔ POINTER VALUE

주소를 얻어 저장하고 간접 참조로 같은 객체에 접근한다

&는 객체를 가리키는 포인터 값을 만들고, *는 포인터 값이 가리키는 대상의 lvalue를 만듭니다.

주소와 간접 참조로 같은 객체에 돌아온다value객체에서주소를구해pointer객체에저장하고pointer를간접참조하면원래value객체를나타낸다. 네노드/네간선의원래desktop왕복관계이며실제주소숫자나복사된새int를뜻하지않는다.value 객체int · 100 → 200주소 취득&value → int*pointer 객체대상 주소값 저장간접 참조*pointer → value같은 객체주소값을 따라 같은 대상을읽거나 쓴다 · 새 복사 아님
value 객체와 pointer 객체 사이의 주소 취득과 간접 참조 순환 왼쪽 value 객체에 주소 연산자를 적용해 오른쪽 pointer 객체에 저장할 포인터 값을 만들고, pointer를 간접 참조해 다시 value 객체를 나타내는 lvalue를 얻는 흐름 value 객체 int · 100 → 200 저장 공간과 수명 주소 취득 &value → int* pointer 객체 int* pointer 대상 주소값 저장 간접 참조 *pointer → int lvalue
  1. 대상 객체

    int value = 100;은 저장 공간과 수명을 가진 객체입니다.

  2. 포인터 값 생성

    &value는 value를 가리키는 int* prvalue를 만듭니다.

  3. 포인터 객체 저장

    int* pointer = &value;가 그 포인터 값을 저장합니다.

  4. 대상 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에 저장한다.

  • value: 100
  • pointer: value 주소
  • 주소를 저장해도 새 정수 객체를 만들지 않는다.
별명 결합

*pointer에 alias 참조를 결합한다.

  • alias: 같은 value
  • value: 100
  • 참조는 가리킨 객체의 또 다른 이름이다.
참조 대입

alias에 200을 대입하면 같은 원본 value가 바뀐다.

  • value: 200
  • *pointer: 200
  • 두 접근은 동일 객체를 읽기 때문에 같은 변경을 관찰한다.

주소 취득과 간접 참조는 객체와 포인터를 왕복한다

value=100의 주소를 pointer에 저장하고 *pointer에 alias를 결합한 뒤 alias=200으로 같은 객체가 변한다.

value 객체에서 주소 연산자로 pointer 값을 만들고 포인터 객체에 저장한 뒤 간접 참조 연산자로 value 객체를 나타내는 lvalue를 얻어 읽거나 쓰는 순환 구조

OBJECT ↔ POINTER VALUE

주소를 얻어 저장하고 간접 참조로 같은 객체에 접근한다

&는 객체를 가리키는 포인터 값을 만들고, *는 포인터 값이 가리키는 대상의 lvalue를 만듭니다.

주소와 간접 참조로 같은 객체에 돌아온다value객체에서주소를구해pointer객체에저장하고pointer를간접참조하면원래value객체를나타낸다. 네노드/네간선의원래desktop왕복관계이며실제주소숫자나복사된새int를뜻하지않는다.value 객체int · 100 → 200주소 취득&value → int*pointer 객체대상 주소값 저장간접 참조*pointer → value같은 객체주소값을 따라 같은 대상을읽거나 쓴다 · 새 복사 아님
value 객체와 pointer 객체 사이의 주소 취득과 간접 참조 순환 왼쪽 value 객체에 주소 연산자를 적용해 오른쪽 pointer 객체에 저장할 포인터 값을 만들고, pointer를 간접 참조해 다시 value 객체를 나타내는 lvalue를 얻는 흐름 value 객체 int · 100 → 200 저장 공간과 수명 주소 취득 &value → int* pointer 객체 int* pointer 대상 주소값 저장 간접 참조 *pointer → int lvalue
  1. 대상 객체

    int value = 100;은 저장 공간과 수명을 가진 객체입니다.

  2. 포인터 값 생성

    &value는 value를 가리키는 int* prvalue를 만듭니다.

  3. 포인터 객체 저장

    int* pointer = &value;가 그 포인터 값을 저장합니다.

  4. 대상 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에 저장한다.

  • value: 100
  • pointer: value 주소
  • 주소를 저장해도 새 정수 객체를 만들지 않는다.
별명 결합

*pointer에 alias 참조를 결합한다.

  • alias: 같은 value
  • value: 100
  • 참조는 가리킨 객체의 또 다른 이름이다.
참조 대입

alias에 200을 대입하면 같은 원본 value가 바뀐다.

  • value: 200
  • *pointer: 200
  • 두 접근은 동일 객체를 읽기 때문에 같은 변경을 관찰한다.
address_and_indirection.cpp
#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의 &가 주소 연산자입니다. 널 포인터나 이미 사라진 객체를 가리키는 포인터에 *를 쓰면 정의되지 않은 동작이므로, 포인터가 올바른 객체를 가리키는지 먼저 확인해야 합니다.

쉼표 연산자

쉼표 연산자 왼쪽, 오른쪽은 왼쪽을 먼저 계산해 그 결과를 버리고, 오른쪽을 계산해 그것을 결과로 합니다. 아래 코드가 세 식을 괄호로 감싼 것은 쉼표 연산자의 우선순위가 모든 연산자 가운데 가장 낮기 때문입니다. 괄호가 없으면 선언 안의 쉼표는 변수 여러 개를 나누는 구분자로 읽혀 컴파일되지 않습니다.

comma.cpp
#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, 초기화 목록의 쉼표는 항목을 나누는 구분자이며, 왼쪽을 버리는 쉼표 연산자가 아닙니다. 이런 자리에서 쉼표 연산자를 쓰려면 괄호로 감싸 하나의 식임을 밝혀야 하며, 대부분은 문장을 나누어 쓰는 편이 읽기 쉽습니다.

연산자마다 나누어 읽기

기본 제공 대입, 조건, sizeof, 주소 취득, 간접 참조, 콤마 연산자를 실제 평가 대상과 결과 타입·값 범주 및 사용 전제로 비교

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을 그대로 갖습니다. 연쇄 대입의 결과는 오른쪽에서 가까운 변수의 타입부터 차례로 정해집니다.

다음 글 연산자 우선순위에서는 지금까지 배운 연산자 전체의 우선순위와 결합 방향을 정리하고, 우선순위와 계산 순서가 어떻게 다른지를 다룹니다.