C언어의 가장 독특한 개념이자, 다른 언어와 가장 차별화되는 기능이 바로 포인터(pointer)
다.
포인터는 단순히 값을 저장하는 게 아니라, 메모리 주소를 저장하는 변수다.
쉽게 말해, "누구를 가리키고 있다"는 느낌이다.
일반적인 변수는 값을 저장한다.
하지만 포인터
는 어떤 변수의 주소를 저장한다.
int a = 10; // 정수 변수 a
int *p = &a; // 포인터 p는 a의 주소를 저장
&a
는 변수 a
의 메모리 주소
*p
는 그 주소에 있는 실제 값을 의미
즉, *p는 a와 같다. 왜냐하면 p는 a를 가리키고 있으니까.
1. 함수에서 값이 아닌 주소를 전달하고 싶을 때
2. 동적 메모리 할당 (heap 영역 제어)
3. 배열이나 문자열 처리
4. C언어의 저수준 메모리 제어 능력 활용
즉, 포인터는 메모리를 직접 다루고 싶은 상황에서 필수적이다.
int a = 5;
int *p = &a;
printf("a의 값: %d\n", a); // 5
printf("p가 가리키는 값: %d\n", *p); // 5
*p = 20;
printf("a의 값(변경 후): %d\n", a); // 20
p
는 a
를 가리킨다.
*p
= 20은 a
를 직접 바꾸는 것과 같다.
함수에서 인자를 포인터로 전달하면, 원본 값을 직접 수정할 수 있다.
void change(int *x) {
*x = 100;
}
int main() {
int a = 10;
change(&a); // a의 주소를 전달
printf("%d\n", a); // 출력: 100
}
값 복사(pass-by-value)가 기본인 C언어에서, 포인터를 쓰면 주소를 직접 넘겨서 원본을 바꿀 수 있게 된다.
Python처럼 참조를 넘기는 방식과 유사하지만, C에서는 명시적으로 해야 한다는 점이 다르다.
int *p;
→ 정수를 가리키는 포인터p = &a;
→ a
의 주소를 p
에 저장*p
→ p
가 가리키는 주소의 값&x
→ 변수 x
의 주소값int *p;
*p = 10; // 초기화되지 않은 포인터에 값 대입
포인터는 어디를 가리킬지 정해져 있지 않으면 위험하다.
항상 선언 후에는 유효한 주소를 넣어야 한다.
배열 이름 자체가 사실상 포인터다. 아래 코드는 사실상 동일하게 동작한다.
int arr[3] = {1, 2, 3};
printf("%d\n", arr[0]); // 1
printf("%d\n", *arr); // 1
printf("%d\n", *(arr + 1)); // 2
배열 이름 arr
은 arr[0]
의 주소와 같다.
따라서 *(arr + i)
는 arr[i]
와 완전히 동일하다.
포인터도 결국 변수니까, 포인터를 가리키는 포인터도 가능하다.
int a = 10;
int *p = &a;
int **pp = &p;
printf("%d\n", **pp); // 출력: 10
주로 함수에서 포인터 자체를 바꾸고 싶을 때, 이중 포인터가 쓰인다.
처음엔 어렵지만, 포인터의 주소를 또 다른 포인터로 다룬다고 이해하면 된다.
정말 필요한 경우가 아니라면, 사용을 지양해야 한다. 어려운 코드일수록 주변 동료와의 소통에 문제를 유발시키기 때문이다.
Python에서는 변수 자체가 객체의 참조를 가지고 있어서, 별도로 포인터를 선언하지 않아도 된다.
하지만 C에서는 값과 주소를 명확히 구분하며, 주소를 직접 다루기 위해 포인터 문법이 반드시 필요하다.
포인터는 초반엔 헷갈리지만, 결국 C언어의 핵심 중 핵심이다.
제대로 익혀두면 배열, 문자열, 함수 전달, 구조체, 동적 메모리 등 모든 것에 적용할 수 있다.
@nullvuild