포인터(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;
- int* a;
- 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 요소 개수를 반환함