11. Pointers

포인터참조 타입의 무언가를 가리키는 파생 자료형이다. 문법은 해당 참조 타입에 *를 붙인다.

double* p0;

여기서 p0은 다른 double형 오브젝트의 메모리를 가리킨다.

11.1. Pointer operations.

C에서는 포인터와 그것이 가리키는 오브젝트간의 상호 관계에 대한 연산자를 제공한다. 포인터는 스칼라로 취급된다.

11.1.1. Address-of and object-of operators.

포인터의 목적 중 하나는 순수 함수가 아닌 함수에 쓰인다. 오브젝트의 메모리 주소를 나타내는 주소값 연산자 &는 포인터 타입을 리턴하는데, 이를 통해 다음과 같은 교환 연산을 할 수 있다.

void double_swap(double* p0, double* p1) {
    double tmp = *p0;
    *p0 = *p1;
    *p1 = tmp;
}

double_swap(&d0, &d1);

double_swap 함수 안의 * 연산자는 해당 주소의 오브젝트 연산자이다. * 문자가 다른 역할을 하고 있음에 주의하라. 포인터 타입의 선언문은 새 자료형 (포인터 타입)을 만들며, 표현식에서는 그것이 포인터가 참조하는 오브젝트를 역참조한다.

Takeaway 2.11.1.1. 부정형이나 널 포인터에 * 연산자를 사용하는 것은 정의되지 않은 행동이다.

구현체에 따라 다르지만 부정형 포인터에 * 연산자를 사용하는 것은 보통 아무 주소나 참조해서 잡기 힘든 버그를 만든다. 널 포인터에 * 연산자를 사용하는 것은 조금 더 나은데, 보통 프로그램을 크래시한다.

11.1.2. Pointer addition.

Takeaway 2.11.1.2. 정상 포인터는 참조 타입으로 이루어진 배열의 첫 번째 원소를 참조한다.

위의 맞바꾸기 함수는 다음과 같다.

void double_swap(double p0[static 1], double p1[static 1]) {
    double tmp = p0[0];
    p0[0] = p1[0];
    p1[0] = tmp;
}

C의 배열의 원소에 접근하는 [] 연산자는 언어 자체에서 제공하는 덮어쓰기 연산자이다. 포인터에 정수 i를 더하면 원래 포인터가 가리키던 배열의 i번째 원소를 가리키는 포인터가 된다.

Takeaway 2.11.1.3. 배열의 길이는 포인터로부터 재구성될 수 없다.

Takeaway 2.11.1.4. 포인터는 배열이 아니다.

그래서 배열을 함수를 통해 넘겨주면 배열의 실제 길이도 같이 넘겨주어야 한다.

11.1.3. Pointer subtraction and difference.

포인터에서 정수를 뺄 수도 있다. 또한, 포인터끼리 뺄 수도 있다. 포인터 차 연산은 두 포인터가 원소 몇 개 만큼 떨어져 있는지에 대한 거리를 정수로 나타낸다. 이것은 두 포인터가 같은 배열의 오브젝트를 가리킬 때에만 허용된다.

Takeaway 2.11.1.5. 배열 오브젝트의 원소를 가리키는 포인터들끼리만 뺄셈을 할 수 있다.

Takeaway 2.11.1.6. 모든 포인터 차는 ptrdiff_t 타입이다.

Takeaway 2.11.1.7. ptrdiff_t 타입으로 부호 있는 위치의 차이값이나 크기 차이값을 나타내라.

Takeaway 2.11.1.8. 출력을 할 때는 void*로 캐스팅한 뒤 %p 포맷을 써라.

11.1.4. Pointer validity.

Takeaway 2.11.1.9. 포인터에는 참값이 있다.

Takeaway 2.11.1.10. 초기화되지 않은 포인터 변수를 최대한 빨리 0으로 세팅하라.

자료형의 타입마다 그 타입에 걸맞는 플랫폼의 비트 표현이 존재한다. 어떤 자료형의 비트 표현이 될 수 없는 표현은 함정 표현이라 한다.

Takeaway 2.11.1.11. 타입의 함정 표현을 가진 오브젝트에 접근하는 것은 정의되지 않은 행동이다.

Takeaway 2.11.1.12. 역참조되었을 때, 포인터가 가리키는 오브젝트는 그 포인터의 기반 타입과 맞아야 한다.

double A[2] = {0.0, 1.0, };
double* p = &A[0];
printf("element %g\n", *p); // referencing object
// p += 3; // invalid pointer addition, undefined behavior
++p; // valid pointer
printf("element %g\n", *p); // referencing object
++p; // valid pointer, no object
printf("element %g\n", *p); // referencing non-object, undefined behavior

Takeaway 2.11.1.13. 포인터는 정상 오브젝트를 참조하거나, 정상 오브젝트의 한 칸 뒤를 참조하거나, 널이어야 한다.

11.1.5. Null pointers.

C에는 어떤 포인터 타입에 대해서도 0 값에 대응할 수 있는 널 포인터라는 개념이 있다.

double const* const nix = 0;
double const* const nax = nix;

여기서 nix와 nax는 0의 값을 갖는 포인터 오브젝트이다. 그러나 널 포인터 상수는 기대와는 다른 것이다. 상수는 const 한정자가 붙은 오브젝트가 아니라 컴파일 타임에 결정되는 상수를 말한다. 때문에 nix와 nax는 널 포인터 상수가 아니다. 또한 상수가 되기 위해서는 정수형 상수 표현식이거나 void* 형이어야 한다. C 표준에서 매크로 NULL의 값에 대해 거는 제한은 매우 느슨하다: 단지 널 포인터 상수이기만 하면 된다. 그러므로 C 컴파일러의 구현체는 NULL로 0U, 0, 0L, ‘\0’, 0UL, (void*) 0 중 어느 것도 택할 수 있다. 구현체에 따라 타입이 다를 수 있는 상수의 사용은 포터블한 프로그래밍을 어렵게 만든다.

Takeaway 2.11.1.14. NULL을 쓰지 말라.

0이나 (void*) 0을 써라.

11.2. Pointers and structures.

구조체에 대한 포인터를 함수 파라미터로 쓰는 것은 구조체 오브젝트를 직접적으로 다룰 수 있게 해 준다. 이 때 -> 연산자가 등장하는데, 이는 구조체 포인터가 가리키는 구조체 오브젝트의 필드를 반환하는 연산자로, * 연산자와 . 연산자의 결합과 같다. 즉, (*a).member는 a->member와 같은 것이다. 이 때 -> 연산자가 리턴하는 결과물은 포인터가 아니라 필드 오브젝트임에 주의하라.

함수를 구조체를 받는 순수 함수로 정의했을 경우 이에 대한 포인터 인자형 래퍼 함수는 다음과 같이 쓸 수 있게 된다.

rat* rat_normalize(rat* rp) {
    if (rp) *rp = rat_get_normal(*rp);
    return rp;
}

구조체 포인터의 특징 중 하나는 그 구조체를 몰라도 쓸 수 있다는 것이다. 이런 불명확 구조는 인터페이스와 구현을 분리할 때 쓰인다.

Takeaway 2.11.2.1. typedef에 포인터를 숨기지 마라.

11.3. Pointers and arrays.

C에서 배열과 포인터의 관계는 다음과 같다: C는 포인터와 배열 원소 접근에 같은 문법을 쓰고 함수의 배열 인자를 포인터로 덮어쓴다.

11.3.1. Array and pointer access are the same.

Takeaway 2.11.3.1. A[i]와 *(A+i) 표현식은 같다.

이것은 A가 포인터일 때 A[i]라는 표현식을 쓰는 것만으로 없던 배열을 허공에서 만들어낸다는 것이 아니다. A가 널 포인터면 A[i]는 잘 크래시된다. A가 배열이라면 *(A + i)에는 C의 가장 중요한 룰인, 배열-포인터 수축이 적용된다.

Takeaway 2.11.3.2(array decay). 배열 A의 평가는 &A[0]을 반환한다.

11.3.2. Array and pointer parameters are the same.

배열-포인터 수축으로 인해, 배열은 함수 인자가 될 수 없다.

Takeaway 2.11.3.3. 함수 선언문에서, 모든 배열 인자는 포인터로 덮어씌워진다.

그러므로 함수 선언문 내 인자에서 배열 표기법이나 포인터 표기법이나 무엇을 선택해도 괜찮지만, 포인터 표기법을 선택할 경우 널 포인터가 올 수 있다는 의도를 전달할 수 있고 배열 표기법을 선택할 경우 널 포인터가 오면 안 된다는 의도를 전달할 수 있다. 의미상으로 배열을 전달할 때에는 배열의 크기도 전달하는 것이 맞을 것이다.

double double_copy(size_t len, double target[len], double const source[len]);

다차원 배열에서는 이야기가 달라지는데, 함수의 다차원 배열에 대한 인자는 배열에 대한 포인터로 덮어씌워진다. 다음 3개의 표기법은 같다.

void matrix_mult(size_t n, size_t k, size_t m, double C[n][m], double A[n][k], double B[k][m]);
void matrix_mult(size_t n, size_t k, size_t m, double (C[n])[m], double (A[n])[k], double (B[k])[m]);
void matrix_mult(size_t n, size_t k, size_t m, double (*C)[m], double (*A)[k], double (*B)[m]);

Takeaway 2.11.3.4. 배열 인자의 가장 안쪽 차원만 포인터로 덮어씌워진다.

Takeaway 2.11.3.5. 배열 인자 이전에 길이 인자를 선언하라.

Takeaway 2.11.3.6. 함수의 배열 인자의 정당성은 프로그래머가 보장해야 한다.

배열의 길이를 컴파일 시간에 알 수 있으면 컴파일러의 도움을 받을 수 있겠지만, 그렇지 못하다면 조심하는 수밖에 없다.

11.4. Function pointers.

주소값 연산자 &는 함수에도 쓰일 수 있다. 룰은 배열-포인터 축소와 비슷하다.

Takeaway 2.11.4.1 (function decay). 함수 인자 내 함수는 따라오는 ()가 없으면 포인터로 덮어씌워진다.

typedef void atexit_function(void);
// two equivalent defintions of the same type, which hides a pointer
typedef atexit_function* atexit_function_pointer;
typedef void (*atexit_function_pointer)(void);
// five equivalent declarations for the same function
void atexit(void f(void));
void atexit(void (*f)(void));
void atexit(atexit_function f);
void atexit(atexit_function* f);
void atexit(atexit_function_pointer f);

위에서 같은 함수를 나타내는 5개의 표기법 중 무엇이 가장 나은지는 논쟁거리이다. 5번째는 좋은 소리를 못 듣는데, 포인터를 타입 내에 숨기기 때문이다.

C 라이브러리에는 함수 파라미터를 받는 함수로 atexit, at_quick_exit 외에 bsearch(이진 탐색)과 qsort(퀵 정렬)이 있다.

typedef int compare_function(void const*, void const*);
void* bsearch(void const* key, void const* base, size_t n, size_t size, compare_function* compar);
void* qsort(void* base, size_t n, size_t size, compare_function* compar);

둘 모두 기반 배열 base를 받아서 작업을 하는데, 이는 void*로 전달되므로 타입 정보를 잃는다. 때문에 배열의 크기 n뿐만 아니라 배열의 기반 타입의 크기인 size도 전달해줘야 한다. compar는 원소간의 비교를 하는 함수이다. 예를 들어 다음과 같이 커스텀 비교 함수를 만들어 전달해줄 수 있다.

int compare_unsigned(void const* a, void const* b) {
    unsigned const* A = a;
    unsigned const* B = b;
    if (*A < *B) return -1;
    else if (*A > *B) return +1;
    else return 0;
}

이렇게 비교 함수를 짜면 안 된다. *A와 *B의 차가 INT_MAX를 넘을 수 있기 때문이다.

int compare_int(void const* a, void const* b) {
    int const* A = a;
    int const* B = b;
    return *A - *B; // may overflow!
}

오브젝트 타입에 대한 포인터에는 범용 포인터로 void*가 존재해 양방향으로 묵시적 형변환이 되지만 함수 포인터에는 그런 것이 없다.

Takeaway 2.11.4.2. 함수 포인터는 정확한 타입으로 사용되어야 한다.

즉 qsort에 다음과 같은 함수는 쓸 수 없다. 컴파일 에러가 난다.

int compare_unsigned(int const* a, int const* b) {
    if (*a < *b) return -1;
    else if (*a > *b) return +1;
    else return 0;
}

() 연산자로 함수 포인터를 호출할 수 있는데, 이는 [] 연산자로 오브젝트 포인터를 역참조하는 것과 비슷한 룰을 가진다.

Takeaway 2.11.4.3. 함수 호출 연산자 ()는 함수 포인터에도 쓸 수 있다.

double f(double a);

// equivalent calls to f, steps in the abstract state machine
f(3); // decay - call
(&f)(3); // address of - call
(*f)(3); // decay - dereference - decay - call
(*&f)(3); // address of - dereference - decay - call
(&*f)(3); // decay - dereference - address of - call

함수 포인터는 거의 함수처럼 쓸 수 있다.

// in a header
typedef int logger_function(char const*, ...);
extern logger_function* logger;
enum logs { log_pri, log_ign, log_ver, log_num };
// in a TU
extern int logger_verbose(char const*, ...);
static int logger_ignore(char const*, ...) {
    return 0;
}
logger_function* logger = logger_ignore;

static logger_function* loggers = {
    [log_pri] = printf,
    [log_ign] = logger_ignore,
    [log_ver] = logger_verbose,
};

위의 로깅 함수에서 두 외부 함수인 printf와 logger_verbose, 하나의 static 함수인 logger_ignore도 배열 초기화에 쓰고 있음을 눈여겨 보라. 저장소 클래스는 함수 인터페이스와 상관이 없다.

함수 포인터를 쓸 때는 함수 호출을 간접적으로 하게 됨을 기억하라. 컴파일러는 함수 포인터가 가리키는 주소를 찾아가 그 함수를 호출해야 한다. 이것에는 오버헤드가 따르며 동작 시간을 극도로 중시해야 하는 코드에서는 피해야 한다.

요점 정리

  • 포인터는 오브젝트와 함수를 참조할 수 있다.
  • 포인터는 배열이 아니며 배열을 참조한다.
  • 함수의 배열 파라미터는 자동적으로 오브젝트 포인터로 덮어씌워진다.
  • 함수의 함수 파라미터는 자동적으로 함수 포인터로 덮어씌워진다.
  • 함수 포인터 타입은 대입될 때나 호출될 때 완전히 일치해야 한다.

답글 남기기

아래 항목을 채우거나 오른쪽 아이콘 중 하나를 클릭하여 로그 인 하세요:

WordPress.com 로고

WordPress.com의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Google photo

Google의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Twitter 사진

Twitter의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

Facebook 사진

Facebook의 계정을 사용하여 댓글을 남깁니다. 로그아웃 /  변경 )

%s에 연결하는 중