포인터 선언

포인터 선언 (Pointer declaration)

C에서 포인터는 메모리 주소를 담는 타입이에요. 어떤 객체를 직접 다루는 대신 그 '위치'만 들고 다니면서, 필요할 때 그 위치로 찾아가는 방식이죠. 이 페이지에서 포인터 선언의 문법과 포인터의 여러 용도를 살펴볼게요.

출처: cppreference

본문

포인터는 함수나 다른 타입의 객체를 가리키는 객체 타입이에요. 한정자(qualifiers)를 덧붙일 수도 있어요. 포인터는 아무 것도 가리키지 않을 수도 있는데, 이는 특별한 null 포인터 값으로 나타내요.

구문 (Syntax)

포인터 선언의 선언 문법에서, 타입 지정자 나열(type-specifier sequence)은 가리키는 타입(포인트된 타입)을 지정해요 (함수 또는 객체 타입일 수 있고 불완전할 수도 있어요). 그리고 선언자(declarator)는 다음 형태를 가져요:

* attr-spec-seq (optional) qualifiers (optional) declarator

여기서 declarator는 선언되는 포인터의 이름을 짓는 식별자일 수 있는데, 다른 포인터 선언자를 포함할 수도 있어요 (그 경우 포인터를 가리키는 포인터를 뜻해요):

float *p, **pp; // p is a pointer to float
                // pp is a pointer to a pointer to float
int (*fp)(int); // fp is a pointer to function with type int(int)

*와 식별자(또는 다른 중첩 선언자) 사이에 나타나는 한정자는 선언되는 포인터의 타입을 한정해요:

int n;
const int * pc = &n; // pc is a non-const pointer to a const int
// *pc = 2; // Error: n cannot be changed through pc without a cast
pc = NULL; // OK: pc itself can be changed

int * const cp = &n; // cp is a const pointer to a non-const int
*cp = 2; // OK to change n through cp
// cp = NULL; // Error: cp itself cannot be changed

int * const * pcp = &cp; // non-const pointer to const pointer to non-const int

attr-spec-seq(C23)는 선언되는 포인터에 적용되는 속성(attributes)의 선택적 목록이에요.

설명 (Explanation)

포인터는 간접(indirection)을 위해 쓰이며, 이는 아주 흔한 프로그래밍 기법이에요. 포인터를 사용하면 참조로 전달(pass-by-reference) 의미론을 구현하고, 동적 저장 기간을 가진 객체에 접근하고, null 포인터 값을 이용해 '옵셔널' 타입을 구현하고, 구조체 간의 집합(aggregation) 관계를 만들고, 함수 포인터로 콜백을 구현하고, void 포인터로 제네릭 인터페이스를 구현하는 등 다양한 일을 할 수 있어요.

객체를 가리키는 포인터 (Pointers to objects)

객체를 가리키는 포인터는, 객체 타입의 표현식(불완전할 수 있음)에 주소 연산자(&)를 적용한 결과로 초기화할 수 있어요:

int n;
int *np = &n; // pointer to int
int *const *npp = &np; // non-const pointer to const pointer to non-const int

int a[2];
int (*ap)[2] = &a; // pointer to array of int

struct S { int n; } s = {1}
int* sp = &s.n; // pointer to the int that is a member of s

포인터는 역참조(indirection, 단항 *) 연산자의 피연산자로 나타날 수 있는데, 이 연산자는 가리키는 객체를 식별하는 lvalue를 돌려줘요:

int n;
int* p = &n; // pointer p is pointing to n
*p = 7; // stores 7 in n
printf("%d\\n", *p); // lvalue-to-rvalue conversion reads the value from n

구조체·공용체 타입의 객체를 가리키는 포인터는, 포인터를 통한 멤버 접근 연산자 ->의 왼쪽 피연산자로 나타날 수도 있어요.

배열-포인터 암시적 변환 덕분에, 배열의 첫 번째 원소를 가리키는 포인터는 배열 타입의 표현식으로 초기화할 수 있어요:

int a[2];
int *p = a; // pointer to a[0]

int b[3][3];
int (*row)[3] = b; // pointer to b[0]

배열의 원소를 가리키는 포인터에 대해 덧셈, 뺄셈, 복합 할당, 증가, 감소 연산자 일부가 정의돼 있어요.

객체를 가리키는 포인터에 대한 비교 연산자는 어떤 상황에서 정의돼요: 같은 주소를 나타내는 두 포인터는 같고, 두 null 포인터 값은 같고, 같은 배열의 원소를 가리키는 포인터들은 그 원소들의 배열 인덱스와 같은 순서로 비교되고, 구조체 멤버를 가리키는 포인터들은 그 멤버들의 선언 순서대로 비교돼요.

많은 구현은 임의 기원을 가진 포인터들에 대해서도 엄격한 전체 순서(strict total ordering)를 제공해요. 예를 들어 포인터들이 연속된('플랫') 가상 주소 공간의 주소로 구현된 경우가 그래요.

함수를 가리키는 포인터 (Pointers to functions)

함수를 가리키는 포인터는 함수의 주소로 초기화할 수 있어요. 함수-포인터 변환 덕분에 주소 연산자는 선택 사항이에요:

void f(int);
void (*pf1)(int) = &f;
void (*pf2)(int) = f; // same as &f

함수와 달리 함수 포인터는 객체이므로, 배열에 저장하고, 복사하고, 할당하고, 다른 함수에 인자로 전달하는 등의 일이 가능해요.

함수를 가리키는 포인터는 함수 호출 연산자의 왼쪽에 쓸 수 있어요. 이렇게 하면 가리키는 함수가 호출돼요:

#include <stdio.h>

int f(int n)
{
    printf("%d\\n", n);
    return n * n;
}

int main(void)
{
    int (*p)(int) = f;
    int x = p(7);
}

함수 포인터를 역참조하면 가리키는 함수에 대한 함수 지정자(function designator)가 나와요:

int f();
int (*p)() = f;    // pointer p is pointing to f
(*p)(); // function f invoked through the function designator
p();    // function f invoked directly through the pointer

함수를 가리키는 포인터에 대한 동등 비교 연산자가 정의돼 있어요 (같은 함수를 가리키면 서로 같다고 비교돼요).

함수 타입의 호환성은 함수 매개변수의 최상위 한정자를 무시하므로, 매개변수가 최상위 한정자에서만 다른 함수 포인터들은 서로 바꿔 쓸 수 있어요:

int f(int), fc(const int);
int (*pc)(const int) = f; // OK
int (*p)(int) = fc;       // OK
pc = p;                   // OK

void 포인터 (Pointers to void)

아무 타입의 객체를 가리키는 포인터는 void 포인터(선택적으로 const·volatile 한정)로 암시적 변환될 수 있고, 그 반대도 가능해요:

int n=1, *p=&n;
void* pv = p; // int* to void*
int* p2 = pv; // void* to int*
printf("%d\\n", *p2); // prints 1

void 포인터는 타입을 알 수 없는 객체를 전달하는 데 쓰이며, 이는 제네릭 인터페이스에서 흔해요: mallocvoid*를 반환하고, qsortconst void* 두 개를 받는 사용자 제공 콜백을 기대하며, pthread_createvoid*를 받고 반환하는 사용자 제공 콜백을 기대해요. 이 모든 경우에, 사용하기 전에 포인터를 올바른 타입으로 변환하는 것은 호출자의 책임이에요.

null 포인터 (Null pointers)

모든 타입의 포인터는 그 타입의 null 포인터 값(null pointer value)으로 알려진 특별한 값을 가져요. 값이 null인 포인터는 객체나 함수를 가리키지 않아요 (null 포인터를 역참조하는 것은 정의되지 않은 동작이에요). 그리고 값이 null인 같은 타입의 모든 포인터들과 서로 같다고 비교돼요.

포인터를 null로 초기화하거나 기존 포인터에 null 값을 할당하려면 null 포인터 상수(NULL, 또는 값이 0인 다른 정수 상수)를 쓰면 돼요. 정적 초기화도 포인터를 null 값으로 초기화해요.

null 포인터는 객체의 부재를 나타낼 수도 있고, 다른 종류의 오류 조건을 나타내는 데 쓰일 수도 있어요. 일반적으로 포인터 인자를 받는 함수는 값이 null인지 확인하고 그 경우를 다르게 처리해야 해요 (예를 들어 free는 null 포인터가 전달되면 아무 일도 하지 않아요).

참고 사항 (Notes)

객체를 가리키는 포인터는 다른 타입의 객체를 가리키는 포인터로 캐스팅할 수 있지만, 객체의 선언 타입과 다른 타입의 포인터를 역참조하는 것은 거의 항상 정의되지 않은 동작이에요. 자세한 내용은 엄격한 앨리어싱(strict aliasing)을 봐요.

포인터를 통해 객체에 접근하는 함수에게, 그 포인터들이 서로 앨리어싱하지 않는다고 알릴 수 있어요. 자세한 내용은 restrict를 봐요.

배열 타입의 lvalue 표현식은 대부분의 맥락에서 배열의 첫 번째 원소를 가리키는 포인터로 암시적 변환돼요. 자세한 내용은 배열을 봐요.

char *str = "abc"; // "abc" is a char[4] array, str is a pointer to 'a'

char 포인터는 문자열을 나타내는 데 흔히 쓰여요. 유효한 바이트 문자열을 나타내려면, 포인터가 char 배열의 원소인 char를 가리켜야 하고, 포인터가 가리키는 원소의 인덱스보다 크거나 같은 어떤 인덱스에 값이 0인 char가 있어야 해요.

더 알아보기