
안녕하세요, C언어를 공부하는 많은 분들이 가장 큰 난관으로 꼽는 개념이 바로 포인터(Pointer)일 것입니다. "포인터는 어렵다", "포인터 때문에 C언어를 포기했다"는 말도 심심찮게 들리죠. 하지만 포인터는 C언어의 강력함을 대표하는 핵심적인 기능이며, 한 번 제대로 이해하면 C언어 실력을 한 단계 업그레이드할 수 있는 마법 같은 도구입니다. 오늘은 포인터의 기본 개념부터 활용까지, 쉽고 명확하게 알아보는 시간을 가지겠습니다!
가장 간단하게 설명하자면, 포인터는 메모리 주소를 값으로 저장하는 변수입니다. 일반 변수가 특정 값을 저장하는 것과 달리, 포인터 변수는 다른 변수의 주소를 저장합니다. 왜 우리는 주소를 알아야 할까요? 컴퓨터는 모든 데이터를 메모리에 저장하고, 각 저장 공간은 고유한 주소를 가집니다. 포인터를 사용하면 이 주소를 통해 메모리에 직접 접근하고 조작할 수 있게 됩니다.
예를 들어, 우리 집 주소를 알면 그 집에 찾아갈 수 있듯이, 변수의 메모리 주소를 알면 그 변수에 저장된 값에 직접 접근할 수 있는 것이죠.
int num = 10; // 정수형 변수 num 선언 및 10으로 초기화
int *ptr; // 정수형 포인터 변수 ptr 선언
ptr = # // num 변수의 주소를 ptr에 저장 (num의 '주소'를 가리킴)
printf("num의 값: %d\n", num); // 10
printf("num의 주소: %p\n", &num); // 0x7ffee5a9c0ac (예시 주소)
printf("ptr의 값 (num의 주소): %p\n", ptr); // 0x7ffee5a9c0ac
printf("ptr이 가리키는 곳의 값: %d\n", *ptr); // 10
위 코드에서:
& 연산자는 address-of 연산자로, 변수의 메모리 주소를 반환합니다.* 연산자는 dereference (역참조) 연산자로, 포인터가 가리키는 주소에 저장된 값을 가져옵니다.포인터 변수를 선언할 때는 어떤 타입의 변수 주소를 저장할 것인지 명시해야 합니다. 이는 포인터가 가리키는 메모리에서 데이터를 읽어올 때, 얼마만큼의 크기를 읽어야 할지 알려주기 위함입니다.
int *intPtr; // int 타입 변수의 주소를 저장할 포인터
char *charPtr; // char 타입 변수의 주소를 저장할 포인터
double *doublePtr; // double 타입 변수의 주소를 저장할 포인터
포인터를 선언할 때는 항상 NULL로 초기화하는 것이 좋은 습관입니다. 초기화되지 않은 포인터(Wild Pointer)는 예상치 못한 메모리 오류를 발생시킬 수 있습니다.
int *ptr = NULL; // 포인터를 NULL로 초기화
C언어에서 배열 이름은 사실상 배열의 첫 번째 요소의 주소(포인터 상수)와 같습니다. 이 때문에 포인터와 배열은 매우 밀접하게 사용됩니다.
int arr[5] = {10, 20, 30, 40, 50};
int *p = arr; // p는 arr[0]의 주소를 가리킴
printf("arr[0]의 주소: %p\n", &arr[0]); // 0x7ffeefbff430
printf("arr의 값 (첫 요소의 주소): %p\n", arr); // 0x7ffeefbff430
printf("p의 값: %p\n", p); // 0x7ffeefbff430
printf("*p: %d\n", *p); // 10
printf("*(p + 1): %d\n", *(p + 1)); // 20 (포인터 연산)
printf("p[2]: %d\n", p[2]); // 30 (포인터를 배열처럼 사용)
p + 1은 주소 값을 1 증가시키는 것이 아니라, 포인터가 가리키는 자료형의 크기만큼 주소를 증가시킵니다. int가 4바이트라면, p + 1은 p의 주소에서 4바이트를 더한 주소가 됩니다. 이는 배열의 다음 요소 주소를 가리키게 되는 것이죠. 이것이 바로 포인터 연산의 핵심입니다.
함수에 인자를 전달하는 방식에는 Call by Value와 Call by Reference가 있습니다. Call by Value는 인자의 값을 복사해서 전달하므로 함수 내에서 값을 변경해도 원본에는 영향을 주지 않습니다.
하지만 포인터를 사용하면 Call by Reference 방식을 구현할 수 있습니다. 즉, 변수의 주소를 함수에 전달하여 함수 내에서 해당 주소의 값을 직접 변경할 수 있습니다. 이는 두 변수의 값을 교환하는 swap 함수를 만들 때 유용하게 사용됩니다.
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int main() {
int x = 5, y = 10;
printf("교환 전: x = %d, y = %d\n", x, y); // x = 5, y = 10
swap(&x, &y); // x와 y의 주소를 전달
printf("교환 후: x = %d, y = %d\n", x, y); // x = 10, y = 5
return 0;
}
swap 함수는 x와 y의 주소를 받아, 해당 주소가 가리키는 값을 직접 변경합니다. 따라서 main 함수에서도 x와 y의 값이 변경된 것을 확인할 수 있습니다.
포인터 변수의 주소를 저장하는 포인터도 존재합니다. 이를 이중 포인터 또는 포인터의 포인터라고 합니다. 주로 동적 할당된 메모리를 함수에서 변경해야 할 때, 또는 포인터 배열을 다룰 때 사용됩니다.
int num = 100;
int *ptr = #
int **dptr = &ptr; // dptr은 ptr의 주소를 가리킴
printf("num의 값: %d\n", num); // 100
printf("*ptr의 값: %d\n", *ptr); // 100
printf("**dptr의 값: %d\n", **dptr); // 100
printf("num의 주소: %p\n", &num); // 0x...A
printf("ptr의 값 (num의 주소): %p\n", ptr); // 0x...A
printf("dptr이 가리키는 곳의 값 (ptr의 주소): %p\n", dptr); // 0x...B (ptr 변수의 주소)
printf("*dptr의 값 (ptr이 가리키는 곳의 값): %p\n", *dptr); // 0x...A (ptr이 저장한 값, 즉 num의 주소)
이중 포인터는 다소 복잡하게 느껴질 수 있지만, 개념은 단순합니다. 주소를 가리키는 변수가 포인터이고, 그 포인터 변수의 주소를 가리키는 변수가 이중 포인터인 것이죠.
C언어 포인터는 처음에는 어렵고 혼란스러울 수 있습니다. 하지만 메모리에 대한 깊은 이해를 제공하고, 효율적인 프로그래밍을 가능하게 하는 강력한 도구입니다. 동적 메모리 할당(malloc, free), 자료구조(링크드 리스트, 트리 등), 운영체제 개발 등 C언어의 많은 고급 기술들은 포인터 없이는 불가능합니다.
& (주소 연산자)와 * (역참조 연산자)의 역할을 명확히 구분하세요.포인터는 C언어의 핵심이자 매력입니다. 꾸준히 연습하고 개념을 익힌다면 여러분도 포인터 마스터가 될 수 있을 겁니다. 포인터와 함께 C언어의 무궁무진한 세계를 탐험해보세요! 다음 시간에는 동적 메모리 할당과 포인터를 활용한 자료구조에 대해 알아보겠습니다.
로그인 후 댓글을 작성할 수 있습니다.