멤버 접근 연산자

멤버 접근 연산자 (Member access operators)

구조체나 공용체의 멤버에 접근하고, 배열의 원소를 집어내고, 포인터가 가리키는 값을 읽거나 그 주소를 얻는 연산들을 한데 묶어서 '멤버 접근 연산자'라고 불러요. 이 페이지에서 C가 제공하는 멤버 접근 연산자들이 각각 어떤 일을 하는지 하나씩 살펴볼게요.

출처: cppreference

본문

멤버 접근 연산자는 피연산자의 멤버에 접근할 수 있게 해줘요.

연산자 연산자 이름 예시 설명
[] 배열 첨자 (array subscript) a[b] 배열 ab번째 원소에 접근
* 포인터 역참조 (pointer dereference) *a 포인터 a가 가리키는 객체나 함수에 접근하기 위해 역참조
& 주소 연산 (address of) &a 객체나 함수 a를 가리키는 포인터를 생성
. 멤버 접근 (member access) a.b 구조체나 공용체 a의 멤버 b에 접근
-> 포인터를 통한 멤버 접근 a->b a가 가리키는 구조체나 공용체의 멤버 b에 접근

첨자 연산 (Subscript)

배열 첨자 표현식은 다음 두 형태 중 하나예요.

pointer-expression [ integer-expression ]   (1)
integer-expression [ pointer-expression ]   (2)

여기서 각 항목은 이렇게 돼요.

  • pointer-expression — '완전한 객체(complete object)에 대한 포인터' 타입의 표현식
  • integer-expression — 정수 타입의 표현식

첨자 연산자의 결과는 lvalue 표현식이고, 그 타입은 pointer-expression이 가리키는 객체의 타입이에요.

정의상 첨자 연산자 E1[E2]*((E1)+(E2))와 정확히 동일해요. 만약 pointer-expression이 배열 표현식이라면 lvalue-to-rvalue 변환을 거쳐 배열의 첫 번째 원소를 가리키는 포인터가 돼요.

포인터와 정수의 덧셈 정의 덕분에, 결과는 integer-expression의 값과 같은 인덱스의 배열 원소가 돼요 (만약 pointer-expression이 어떤 배열의 i번째 원소를 가리키고 있었다면, 결과의 인덱스는 i에 integer-expression의 값을 더한 값이 돼요).

참고: 다차원 배열에 대한 자세한 내용은 배열 문서를 봐요.

#include <stdio.h>
int main(void)
{
    int a[3] = {1,2,3};
    printf("%d %d\\n", a[2],  // n == 3
                      2[a]); // same, n == 3
    a[2] = 7; // subscripts are lvalues

    int n[2][3] = {{1,2,3},{4,5,6}};
    int (*p)[3] = &n[1]; // elements of n are arrays
    printf("%d %d %d\\n", (*p)[0], p[0][1], p[0][2]); // access n[1][] via p
    int x = n[1][2]; // applying [] again to the array n[1]
    printf("%d\\n", x);

    printf("%c %c\\n", "abc"[2], 2["abc"]); // string literals are arrays too
}

출력:

3 3
4 5 6
6
c c

역참조 (Dereference)

역참조(또는 간접(indirection)) 표현식은 다음 형태예요.

* pointer-expression

여기서 pointer-expression은 아무 포인터 타입의 표현식이에요.

만약 pointer-expression이 함수를 가리키는 포인터라면, 역참조 연산자의 결과는 그 함수에 대한 함수 지정자(function designator)가 돼요.

만약 pointer-expression이 객체를 가리키는 포인터라면, 결과는 가리키는 객체를 지정하는 lvalue 표현식이 돼요.

null 포인터, 수명이 끝난 객체(댕글링 포인터)를 가리키는 포인터, 정렬이 어긋난 포인터, 또는 불확정 값을 가진 포인터를 역참조하는 것은 정의되지 않은 동작이에요. 다만 &*E처럼 역참조 연산자의 결과에 주소 연산자를 적용해서 상쇄되는 경우는 예외예요.

#include <stdio.h>
int main(void)
{
    int n = 1;
    int* p = &n;
    printf("*p = %d\\n", *p); // the value of *p is what's stored in n
    *p = 7; // *p is lvalue
    printf("*p = %d\\n", *p);
}

출력:

*p = 1
*p = 7

주소 연산 (Address of)

주소 연산 표현식은 다음 형태 중 하나예요.

& function            (1)
& lvalue-expression   (2)
& * expression        (3)
& expression [ expression ]  (4)
  1. 함수의 주소
  2. 객체의 주소
  3. 특수한 경우: &*가 서로 상쇄되어 둘 다 평가되지 않아요
  4. 특수한 경우: &[]에 내포된 *가 상쇄되고, []에 내포된 덧셈만 평가돼요

여기서 lvalue-expression은 비트 필드가 아니고 register 저장 클래스가 아닌, 아무 타입의 lvalue 표현식이에요.

주소 연산자는 피연산자의 lvalue가 아닌 주소를 만들어내고, 그 값을 피연산자 타입에 대한 포인터를 초기화하는 데 쓸 수 있어요. 피연산자가 함수 지정자(1)라면 결과는 함수 포인터가 되고, 객체(2)라면 결과는 객체 포인터가 돼요.

피연산자가 역참조 연산자라면 아무 동작도 하지 않아요 (그래서 null 포인터에 &*를 적용해도 괜찮아요). 다만 결과가 lvalue가 아니라는 점만 달라져요.

피연산자가 배열 인덱스 표현식이라면, 배열-포인터 변환과 덧셈 외에는 아무 동작도 하지 않아요. 그래서 크기 N인 배열에 대해 &a[N]은 유효해요 (배열 끝 하나 너머의 포인터를 얻는 건 괜찮지만, 그걸 역참조하는 건 안 돼요 — 그런데 이 식에서는 역참조가 상쇄되므로 문제없어요).

int f(char c) { return c;}
int main(void)
{
   int n = 1;
   int *p = &n; // address of object n
   int (*fp)(char) = &f; // address of function f
   int a[3] = {1,2,3};
   int *beg=a, *end=&a[3]; // same as end = a+3
}

멤버 접근 (Member access)

멤버 접근 표현식은 다음 형태예요.

expression . member-name

여기서 각 항목은 이렇게 돼요.

  • expression — 구조체나 공용체 타입의 표현식
  • member-nameexpression이 지정하는 구조체나 공용체의 멤버를 가리키는 식별자

멤버 접근 표현식은 왼쪽 피연산자가 지정하는 구조체나 공용체의 해당 멤버를 지정해요. 값 범주(value category)는 왼쪽 피연산자와 같아요.

왼쪽 피연산자가 constvolatile 한정이라면 결과도 한정돼요. 왼쪽 피연산자가 원자(atomic) 타입이면 동작이 정의되지 않아요.

참고: 구조체나 공용체 타입의 객체를 이름 짓는 식별자 외에도, 할당, 함수 호출, 콤마 연산자, 조건 연산자, 복합 리터럴 같은 표현식도 구조체나 공용체 타입을 가질 수 있어요.

#include <stdio.h>
struct s {int x;};
struct s f(void) { return (struct s){1}; }
int main(void)
{
    struct s s;
    s.x = 1; // ok, changes the member of s
    int n = f().x; // f() is an expression of type struct s
//  f().x = 1; // Error: this member access expression is not an lvalue

    const struct s sc;
//  sc.x = 3;  // Error: sc.x is const, can't be assigned

    union { int x; double d; } u = {1};
    u.d = 0.1; // changes the active member of the union
}

포인터를 통한 멤버 접근 (Member access through pointer)

포인터를 통한 멤버 접근 표현식은 다음 형태예요.

expression -> member-name

여기서 각 항목은 이렇게 돼요.

  • expression — 구조체나 공용체에 대한 포인터 타입의 표현식
  • member-nameexpression이 가리키는 구조체나 공용체의 멤버를 가리키는 식별자

이 표현식은 왼쪽 피연산자가 가리키는 구조체나 공용체 타입의 해당 멤버를 지정해요. 값 범주는 항상 lvalue예요.

왼쪽 피연산자가 가리키는 타입이 constvolatile 한정이라면 결과도 한정돼요. 가리키는 타입이 원자(atomic) 타입이면 동작이 정의되지 않아요.

#include <stdio.h>
struct s {int x;};
int main(void)
{
    struct s s={1}, *p = &s;
    p->x = 7; // changes the value of s.x through the pointer
    printf("%d\\n", p->x); // prints 7
}

결함 보고서 (Defect reports)

다음 동작 변경 결함 보고서는 이미 발표된 C 표준에 소급 적용됐어요.

DR 적용 대상 발표 당시 동작 올바른 동작
DR 076 C89 불필요한 간접(indirection)은 &로 상쇄할 수 없었음 상쇄 가능하게 만듦

더 알아보기