..

포인터(Pointer)

  • 메모리 주소를 저장하는 변수

포인터

 

포인터 선언법

int num = 0;
int* p = #

printf("%d", *p); // 0 출력, 포인터 p의 num 참조

 

*

  • 선언 할때 역할 -> 포인터 타입을 만든다
  • 사용 할때 역할 -> 포인터가 가리키는 주소에 저장된 값을 간접 참조한다

&

  • 사용 할때 역할 -> 변수의 주소값을 반환한다 (C++에서는 참조 타입을 선언할 때도 사용된다)

[]

  • arr[i] == *(arr + i) 완전히 동일하다

->

  • 내가 가리키는 객체의 멤버 접근

.

  • 내가 가진 객체의 멤버 접근

 

포인터 int* p와 int *p 선언 차이

  • 현대 C++ 스타일에서는 *를 타입에 붙이는 것을 선호함
    • int* a, b; // 이건 혼란스러움. a는 포인터지만 b는 int
    • 이 때문에 여러 변수 선언 시에는 한 줄에 하나씩 쓰는 것이 좋음
      • int* a;
        int b;
  • C 스타일에서는 *를 변수명에 붙이는 것을 선호함

 

포인터는 완전한 객체 타입이다

  • 포인터 타입은 complete object type
    즉, 포인터 자체는 크기와 할당 가능한 메모리 공간을 가지는 타입이다

 

시스템이 메모리를 몇 비트로 주소 지정하는지에 따라 포인터 크기도 달라진다

  • 32비트 시스템: 4바이트
  • 64비트 시스템: 8바이트

가리키는 대상의 타입(char, int, double 등)에 관계없이 크기는 같다

 

배열과 포인터

배열 타입의 수식(배열 이름도 여기에 해당한다)은 그 배열의 첫 번째 원소를 가리키는 포인터로 자동 변환

  • &arr[0] = arr
  • arr[i] == *(arr + i) == *(i + arr) == i[arr]
    (C에서 배열 인덱스 연산 a[b] 는 내부적으로 *(a + b)로 정의되기 때문)

 

배열 포인터와 포인터 배열

#include <stdio.h>

int main() {
    // 포인터 배열: int*가 3개 있는 배열
    int a = 10, b = 20, c = 30;
    int* ptrArr[3];   // 포인터 배열

    ptrArr[0] = &a;
    ptrArr[1] = &b;
    ptrArr[2] = &c;

    printf("포인터 배열 사용:\n");
    for (int i = 0; i < 3; i++) {
        printf("ptrArr[%d] = %d\n", i, *ptrArr[i]);  // 10, 20, 30
    }

    // 배열 포인터: int[3] 하나를 가리키는 포인터
    int arr[3] = {100, 200, 300};
    int (*arrPtr)[3] = &arr;  // 배열 포인터

    printf("\n배열 포인터 사용:\n");
    for (int i = 0; i < 3; i++) {
        printf("(*arrPtr)[%d] = %d\n", i, (*arrPtr)[i]); // 100, 200, 300
    }

    return 0;
}
  • int* arr[N] = 포인터들의 배열
    • 접근 방식: *arr[i]
    • 해석
      • []가 *보다 우선순위가 높다
      • 따라서 먼저 arr[i]가 해석
  • int (*p)[N] = 배열 하나를 가르키는 포인터 (괄호 O)
    • 접근 방식: (*p)[i]
    • 해석
      • 괄호가 있으므로 *p가 먼저 해석됨

 

int (*arrPtr)[3] = &arr;에서 [3]이 필요한 이유

  • 컴파일러가 타입 체크와 포인터 연산을 정확하게 수행할 수 있기 때문

 

arr과 &arr과 &arr[0]의 차이

표현식 의미 타입
arr 첫 번째 요소 주소 int*
&arr 배열 전체 주소 int (*)[N]
&arr[0] 첫 번째 요소 주소 int*

같은 주소를 반환하지만 타입이 다르다

 

함수와 포인터

void foo(int arr[]);
void foo(int* arr);
  • 배열을 매개변수로 선언하면, 배열 전체가 복사되는 것이 아니라, 첫 번째 원소를 가리키는 포인터가 전달된다

 

함수 포인터

반환타입 (*포인터이름)(매개변수타입);

int add(int a, int b) {
    return a + b;
}

int (*fp)(int, int);  // 함수 포인터 선언
fp = add;             // 함수 주소 대입
int result = fp(3, 5);  // 함수 호출
  • 함수 포인터의 사용 예시

 

void f() {
    printf("hello\n");
}

void (*fp)() = f;     // 또는 = &f;
  • 함수 이름은 대부분의 표현식에서 자동으로 함수 포인터로 변환되지만,
    모든 경우에 그런 것은 아니며, sizeof, & 등 특정 문맥에서는 변환되지 않음

  • decay란 배열이나 함수 이름이 자동으로 포인터로 변환되는 것을 말한다

  • f(); (*f)(); (****f)(); fp(); (*fp)(); (***************fp)();
    위 문법 모두 함수 f를 호출한다

  • (*f)(), (**f)(), (***f)() 등 모두 f()로 처리됨
    함수 이름 f는 자동으로 &f (함수의 주소)로 decay되서 f = &f

  • fp = &f; fp = &&f;에서 &&f 틀린 호출법이다
    &&f는 label의 주소, 즉 goto용 레이블의 주소 → 전혀 다른 개념

  • 함수 포인터는 정확한 시그니처가 일치해야만 안전하게 사용할 수 있다
    표준 C에서 undefined behavior (정의되지 않은 동작)이라고 본다

 

int f1(int);
void f2(double);

int (*fp)(int);
fp = f1;        // 작동
fp = f2;        // 컴파일러는 통과해도, 표준적으로는 정의되지 않은 동작 (UB)

 

포인터 증감 연산

	int SomeArray[10];

	int* pLocation5 = &SomeArray[5];
	int* pLocation0 = &SomeArray[0];

	printf("%p \n", (void*)pLocation5);        // &SomeArray[5]
	printf("%p \n", (void*)pLocation0);        // &SomeArray[0]
	printf("%td \n", pLocation5 - pLocation0); // 출력 결과 5

int형 배열이라 20바이트 차이가 나지면 포인터끼리 연산했을때 바이트가 아니라 int 요소 개수를 반환함