기타 연산자
대입·조건·sizeof·주소·간접 참조·콤마 연산자의 평가 순서, 결과 타입과 값 범주, 사용 전제를 구분합니다.
이 장에서는 값을 저장하는 대입, 두 경로 중 하나를 고르는 조건 연산자, 타입과 객체의 크기를 묻는 sizeof, 주소와 객체를 오가는 &·*, 두 표현식을 순서대로 잇는 콤마 연산자를 다룹니다.
같은 기호라도 선언 문법과 표현식의 연산자는 구분해야 합니다. 또한 결합 방향은 식이 묶이는 방법이고, 평가 순서는 실제 계산과 부수 효과의 순서입니다.
처음 읽을 때는 lvalue를 age처럼 객체를 가리키는 식, prvalue를 계산으로 얻는 값으로 구별합니다. glvalue는 lvalue 등을 포함하는 넓은 분류이며 뒤의 참조·이동 설명에서 다시 다룹니다. 이 절에서는 전문 이름보다 “어느 객체가 바뀌고 어떤 값이 남는가”를 먼저 확인하세요. 짧은 예는 원리를 보여 주는 부분 코드이므로, 미리 정의되지 않은 이름이나 함수 선언만 있는 예를 그대로 실행 파일로 빌드하지 않습니다.
초기화와 대입
선언과 함께 첫 값을 주는 코드는 초기화입니다. 이미 존재하는 객체의 값을 바꾸는 = 표현식이 대입입니다.
int age = 20; // copy-initialization
age = 21; // assignment expression기본 제공 대입 연산자는 수정 가능한 lvalue를 왼쪽 피연산자로 요구합니다. 오른쪽 피연산자는 왼쪽 피연산자보다 먼저 평가되며, 오른쪽 값은 왼쪽 객체의 타입으로 변환된 뒤 저장됩니다. 대입식의 결과는 왼쪽 객체를 가리키는 lvalue입니다.
대입 연산자는 오른쪽에서 왼쪽으로 묶입니다. 따라서 다음 식은 x = (y = (z = 10))으로 해석됩니다. 이것은 모든 하위 표현식을 일반적으로 오른쪽부터 평가한다는 뜻이 아닙니다.
int x = 0;
int y = 0;
int z = 0;
x = y = z = 10;
int n = 0;
double d = 0.0;
d = n = 3.8; // n은 3, d는 3.0두 번째 연쇄에서는 3.8이 int로 변환되어 n에 저장되고, n = 3.8의 결과가 다시 읽혀 double로 변환됩니다. 각 대입 단계가 왼쪽 타입의 변환 경계입니다.
대입 연쇄는 묶임, 평가, 변환, 저장을 단계별로 읽는다
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에 저장한다. |
|
|
복합 대입
기본 제공 E1 op= E2는 E1 = E1 op E2와 같은 동작을 하되 E1을 한 번만 평가합니다. 단순한 문자열 치환이나 성능 힌트가 아니라, 왼쪽 위치를 한 번만 찾는 언어 계약입니다.
std::size_t index();
int values[4]{};
values[index()] += 3; // index()는 한 번 호출된다+=, -=, *=, /=, %=와 비트 복합 대입은 해당 연산의 타입·범위 전제를 그대로 따릅니다. 예를 들어 0으로 나누기, 유효하지 않은 시프트 수, 표현 범위를 벗어난 signed 산술은 복합 대입으로 바꾼다고 안전해지지 않습니다.
조건 연산자
조건 연산자 condition ? second : third는 첫 피연산자를 bool로 문맥 변환합니다. 조건을 먼저 평가한 뒤 두 번째와 세 번째 피연산자 중 선택된 하나만 평가합니다.
std::string label = score >= 60 ? "합격" : "불합격";
int& selected = use_left ? left : right;
selected = 0; // 선택된 객체를 바꾼다결과가 언제나 새 임시 값인 것은 아닙니다. 두 선택지가 같은 타입·값 범주의 glvalue라면 조건식도 그 값 범주를 유지할 수 있습니다. 그 밖에는 두 피연산자의 변환 규칙으로 공통 결과 타입을 정합니다. 복잡한 부수 효과가 두 분기에 섞이면 if 문이나 이름 있는 함수로 분리하는 편이 낫습니다.
sizeof
sizeof(type) 또는 sizeof expression은 해당 타입 객체가 차지하는 바이트 수를 std::size_t prvalue로 만듭니다. 표현식 피연산자는 미평가 피연산자이므로 일반적인 부수 효과가 실행되지 않습니다.
int value = 7;
static_assert(sizeof(value++) == sizeof(int));
// value++는 실행되지 않아 value는 여전히 7이다.
int numbers[5]{};
static_assert(sizeof(numbers) == 5 * sizeof(numbers[0]));sizeof(char)는 항상 1이지만 C++의 1바이트가 반드시 8비트라는 뜻은 아닙니다. 다른 기본 타입의 크기와 클래스 패딩은 구현에 따라 달라질 수 있습니다.
배열 피연산자에는 배열-포인터 변환이 적용되지 않으므로 실제 배열의 전체 크기를 얻습니다. 그러나 함수 매개변수로 받은 포인터에 sizeof를 적용하면 포인터 크기만 얻습니다. 배열 요소 수는 가능하면 std::size(numbers)처럼 의도를 직접 드러내는 표준 도구를 사용합니다.
주소 취득과 간접 참조
표현식의 단항 &는 객체나 함수의 주소를 가리키는 포인터 값을 만듭니다. 표현식의 단항 *는 포인터를 따라가 그 대상 객체나 함수를 나타내는 lvalue를 만듭니다.
int value = 100;
int* pointer = &value;
int& alias = *pointer;
alias = 200; // value도 200int* pointer의 *는 포인터 타입을 만드는 선언자 일부이고, *pointer의 *는 간접 참조 연산자입니다. 간접 참조에는 포인터가 허용된 대상을 가리킨다는 전제가 필요합니다. null, 수명이 끝난 객체, 또는 그 밖의 유효하지 않은 대상을 값처럼 읽거나 쓰면 정의되지 않은 동작으로 이어질 수 있습니다.
주소를 출력한 문자열의 형식과 실제 값은 구현과 실행 환경에 따라 달라질 수 있으므로 특정 16진수 주소를 프로그램 계약으로 삼지 않습니다.
주소 취득과 간접 참조는 객체와 포인터를 왕복한다
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";
}value와 saved는 각각 int 객체입니다. pointer는 value를 가리키는 주소값을 저장하며 alias는 그 value의 다른 이름입니다. alias=200은 새 int에 값을 복사하는 문장이 아니라 기존 value에 쓰는 대입입니다. 출력은 200 200 100이고 saved는 복사해 둔 100을 유지합니다. 실제 주소의 숫자는 필요하지 않습니다.
저장 대상을 바꾸어 보기
다른 줄을 그대로 두고 pointer=&value만 pointer=&saved로 바꾸면 출력은 무엇인가요? 별도의 int n=7; 뒤 sizeof(n++)를 사용하면 n은 바뀔까요? 실행 전에 실제로 쓰는 객체와 미평가 피연산자를 구별해 적습니다.
다른 대상과 미평가 피연산자 풀이
참조 alias도 saved를 가리키므로 대입 후 value=100, saved=200, *pointer=200입니다. 출력은 100 200 200입니다. sizeof(n++)는 이 정수 표현식을 실행하지 않으므로 n은 7입니다. 크기의 숫자는 구현에 따라 달라도 값이 증가하지 않는다는 결론은 같습니다. null 포인터로 바꾸어 간접 참조를 실험하는 것은 허용된 입력이 아닙니다.
콤마 연산자
기본 제공 콤마 연산자 left, right는 왼쪽 피연산자를 먼저 평가하고 그 값을 버린 뒤 오른쪽 피연산자를 평가합니다. 결과의 타입·값·값 범주는 오른쪽 피연산자의 것을 그대로 따릅니다.
int x = 10;
int y = 20;
int result = (++x, y += 5, x + y); // x=11, y=25, result=36
for (int i = 0, j = 10; i < 5; ++i, --j) {
// 선언부의 첫 콤마는 선언자 구분자,
// 반복식의 콤마는 기본 제공 콤마 연산자다.
}함수 인자 목록, 선언자 목록, 초기화 목록, 템플릿 인자 목록의 콤마는 문법 구분자이며 여기서 설명한 콤마 연산자가 아닙니다. 그런 문맥에서 콤마 연산자를 쓰려면 괄호로 하나의 표현식임을 밝혀야 합니다.
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 · 콤마
평가 · 왼쪽을 끝낸 뒤 값을 버리고 오른쪽을 평가합니다.
결과 · 오른쪽의 타입·값·값 범주를 유지합니다.
이 비교는 기본 제공 연산자 계약입니다. 함수 인자·선언자·초기화 목록의 콤마는 구분자이며, 결합 방향만으로 일반 평가 순서를 추론하지 않습니다.
짧은 식이라도 먼저 무엇을 평가하는가, 어떤 결과 타입과 값 범주를 만드는가, 어떤 객체를 바꾸는가, 유효성 전제가 무엇인가를 분리해 읽습니다. 우선순위와 결합 방향은 다음 장에서 별도로 다룹니다.