본문으로 건너뛰기

안동민 개발노트

본문 시작

기타 연산자

대입·조건·sizeof·주소·간접 참조·콤마 연산자의 평가 순서, 결과 타입과 값 범주, 사용 전제를 구분합니다.

이 장에서는 값을 저장하는 대입, 두 경로 중 하나를 고르는 조건 연산자, 타입과 객체의 크기를 묻는 sizeof, 주소와 객체를 오가는 &·*, 두 표현식을 순서대로 잇는 콤마 연산자를 다룹니다.

같은 기호라도 선언 문법표현식의 연산자는 구분해야 합니다. 또한 결합 방향은 식이 묶이는 방법이고, 평가 순서는 실제 계산과 부수 효과의 순서입니다.

초기화와 대입

선언과 함께 첫 값을 주는 코드는 초기화입니다. 이미 존재하는 객체의 값을 바꾸는 = 표현식이 대입입니다.

초기화와 대입 구분
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.8int로 변환되어 n에 저장되고, n = 3.8의 결과가 다시 읽혀 double로 변환됩니다. 각 대입 단계가 왼쪽 타입의 변환 경계입니다.

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

ASSIGNMENT IS A STORE CONTRACT

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

x = y = z = 10의 네 단계

  1. 구문 묶임

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

  2. z = 10

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

  3. y = z

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

  4. x = y

    같은 계약으로 x를 갱신합니다. 일반적인 모든 하위식의 평가 방향을 뜻하지는 않습니다.

단계마다 변환

d = n = 3.8에서 먼저 n3, 이어 d3.0을 저장합니다.

결과는 왼쪽 lvalue

기본 제공 대입식은 왼쪽 피연산자의 타입이며 바로 그 객체를 가리킵니다.

복합 대입의 차이

E1 op= E2는 대응하는 대입과 같은 동작을 하되 위치 표현식 E1을 한 번만 평가합니다.

int age = 20;은 초기화이고, 이미 존재하는 객체에 쓰는 age = 21;이 대입 표현식입니다.

복합 대입

기본 제공 E1 op= E2E1 = 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로 만듭니다. 표현식 피연산자는 미평가 피연산자이므로 일반적인 부수 효과가 실행되지 않습니다.

sizeof의 미평가 피연산자와 배열 크기
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도 200

int* pointer*는 포인터 타입을 만드는 선언자 일부이고, *pointer*는 간접 참조 연산자입니다. 간접 참조에는 포인터가 허용된 대상을 가리킨다는 전제가 필요합니다. null, 수명이 끝난 객체, 또는 그 밖의 유효하지 않은 대상을 값처럼 읽거나 쓰면 정의되지 않은 동작으로 이어질 수 있습니다.

주소를 출력한 문자열의 형식과 실제 값은 구현과 실행 환경에 따라 달라질 수 있으므로 특정 16진수 주소를 프로그램 계약으로 삼지 않습니다.

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

OBJECT ↔ POINTER VALUE

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

value 객체와 pointer 객체 사이의 주소 취득과 간접 참조 순환 왼쪽 value 객체에 주소 연산자를 적용해 오른쪽 pointer 객체에 저장할 포인터 값을 만들고, pointer를 간접 참조해 다시 value 객체를 나타내는 lvalue를 얻는 흐름 value 객체 int · 100 → 200 저장 공간과 수명 주소 취득 &value → int* pointer 객체 int* pointer value를 가리키는 값 저장 간접 참조 *pointer → int lvalue
  1. 대상 객체

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

  2. 포인터 값 생성

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

  3. 포인터 객체 저장

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

  4. 대상 lvalue 복구

    *pointervalue를 나타내므로 읽거나 대입할 수 있습니다.

  • 실제 저장 객체
  • 주소와 대상 사이의 변환

간접 참조의 전제는 포인터가 허용된 대상을 가리키는 것입니다. 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) {
    // 선언부의 첫 콤마는 선언자 구분자,
    // 반복식의 콤마는 기본 제공 콤마 연산자다.
}

함수 인자 목록, 선언자 목록, 초기화 목록, 템플릿 인자 목록의 콤마는 문법 구분자이며 여기서 설명한 콤마 연산자가 아닙니다. 그런 문맥에서 콤마 연산자를 쓰려면 괄호로 하나의 표현식임을 밝혀야 합니다.

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

EVALUATION → RESULT → PRECONDITION

기호 이름보다 먼저 실제로 평가되는 피연산자, 만들어지는 결과, 안전하게 쓰기 위한 전제를 확인합니다.

E1 = E2 · 대입

평가 · E2E1보다 먼저입니다.

결과 · 변환 후 저장하며 결과는 E1 lvalue입니다.

c ? a : b · 조건

평가 · c 뒤 선택된 한 분기만 평가합니다.

결과 · 두 분기의 타입·값 범주 규칙으로 정합니다.

sizeof e · 크기

평가 · 표현식 e는 미평가 피연산자입니다.

결과 · 바이트 수인 std::size_t prvalue입니다.

&object · 주소

입력 · 객체나 함수를 나타내는 lvalue입니다.

결과 · 대상을 가리키는 포인터 prvalue입니다.

*pointer · 간접 참조

평가 · 포인터 값을 따라 대상을 찾습니다.

결과 · 대상 타입의 lvalue이며 유효한 대상이 전제입니다.

left, right · 콤마

평가 · 왼쪽을 끝낸 뒤 값을 버리고 오른쪽을 평가합니다.

결과 · 오른쪽의 타입·값·값 범주를 유지합니다.

이 비교는 기본 제공 연산자 계약입니다. 함수 인자·선언자·초기화 목록의 콤마는 구분자이며, 결합 방향만으로 일반 평가 순서를 추론하지 않습니다.

짧은 식이라도 먼저 무엇을 평가하는가, 어떤 결과 타입과 값 범주를 만드는가, 어떤 객체를 바꾸는가, 유효성 전제가 무엇인가를 분리해 읽습니다. 우선순위와 결합 방향은 다음 장에서 별도로 다룹니다.