C 언어 포인터 - 메모리를 직접 다루는 법
C 언어를 배우면서 가장 어렵다고 느끼는 부분이 포인터다. 하지만 포인터를 이해하면 메모리가 어떻게 동작하는지 알 수 있고, 다른 언어를 배울 때도 도움이 된다. 이 글에서는 포인터의 기본 개념과 주의할 점을 정리한다.
c 에서 포인터를 통해 직접 데이터의 주소에 접근할 수 있으며, 이러한 특성 때문에 C 언어가 low level 언어라고 불린다.
int main(void)
{
int num =5; // 정수형 변수 num을 생성한다.
int * pnum = # // 정수형 변수 num을 가리키는 포인터 변수 pnum을 선언한다.
*pnum = 10; // pnum이 가리키는 변수에 10을 저장하라
}
이처럼 특정 자료를 가리키는 자료형을 정의 할 수 있고, 이를 포인터라 한다.
포인터는 특정 자료형을 가리키고 있다.
여기서 포인터는 어떤 변수를 가리키는 그 자체이므로 메모리의 특정 주소값을 가진다.
때문에 포인터를 선언할 때에는 매우 신중해야 하는데 선언 후 아무 값도 할당 하지 않는다면 포인터는 쓰레기 값으로 초기화 되기 때문이다.
int *pnum = 399;
가령 위와 같은 코드는 매우 위험한데 이는 pnum이라는 포인터에 399를 할당하고 있고, 우리는 399 의 주소값이 얼마나 중요한 주소인지 전혀 이해하고 있지 못하기 때문이다. 이를 해결하기 위해 반드시 우리는 해당 포인터 값이 이상한 값을 가리키지 않도록 해야 하는데 주로 다음과 같이 널 포인터를 지정해 준다.
int *ptr2 = 0;
int *ptr2 = NULL;
위와 같이 NULL 혹은 0 으로 초기화 하면 시스템은 이것을 0 번째 주소값으로 인식하지 않고 아무것도 가리키지 않는 포인터를 만들어 낸다.
포인터와 배열의 관계
c 언어에서 배열의 이름은 그 자체로 포인터 이며, 정확히 말하면 배열의 첫번째 요소의 주소값이다.
int arr[3] = {1, 2, 3};
printf("%p", arr); // 주소값이 나온다.
printf("%p", arr[0]); // 위와 같은 주소값이 나온다. 즉, 배열의 주소는 첫번째 값의 주소를 의미한다.
변수 형태의 문자열 vs 상수형태의 문자열
char str1[] = "my string";
char * str2 = "your string";
str1[0] = "x";// 문자열 변경 성공
str2[0] = "x"; // 문자열 변경 실패
위처럼 모든 문자열 혹은 배열을 선언하면 그 자체는 그 문자열 혹은 배열의 첫번째 요소의 주소값이 된다.
하지만 위에서 처럼 문자열 혹은 배열 자체를 선언하는 경우와 그것의 주소값을 선언하는 경우에 따라 그 값의 변경 가능 유무가 결정이 된다.
위의 예처럼 str1의 경우 문자열 자체를 저장한 변수형 문자열이며, str2의 경우 문자열에 대한 주소값을 저장하는 상수 형태의 문자열이기 때문에 가리키는 값의 내용이 변경될 수 없다.
포인터 변수로 이루어진 배열
다음과 같이 포인터 변수로 이루어진 배열을 선언할 수 있다.
char * strArr[3] = {"Simple", "String", "Array"};
printf("%s \n",strArr[0]) // Simple
위처럼 "" 로 감싸진 문자열은 그 자체로 주소값을 나타내기 때문에 위와 같은 결과가 나오게 된다.
큰 따옴표로 묶여서 표현되는 문자열은 그 형태에 상관없이 메모리 공간에 저장된 후 그 주소값이 반환된다.
마무리
포인터의 핵심 개념을 정리하면:
| 개념 | 설명 |
|---|---|
| 포인터 | 메모리 주소를 저장하는 변수 |
& 연산자 | 변수의 주소를 반환 |
* 연산자 | 포인터가 가리키는 값에 접근 |
| NULL 포인터 | 아무것도 가리키지 않는 안전한 상태 |
| 배열 이름 | 첫 번째 요소의 주소 (포인터와 유사) |
개인적인 생각: 포인터가 어려운 이유는 "변수가 값을 직접 가지는 게 아니라 주소를 가진다"는 개념 전환이 필요하기 때문이다. Python이나 JavaScript에서 C로 오면 특히 헷갈린다.
실무에서 C를 직접 쓸 일은 줄었지만, 포인터 개념은 여전히 중요하다:
- Java/Python의 참조(reference)가 결국 포인터의 추상화
- 메모리 누수, 댕글링 포인터 같은 버그 이해
- 시스템 프로그래밍, 임베디드 개발에 필수
포인터로 인한 버그(segmentation fault, buffer overflow)는 보안 취약점의 주요 원인이기도 하다. Rust 같은 언어가 나온 이유도 C/C++의 메모리 안전성 문제 때문이다.