C언어와 C++은 포인터를 사용해서 다른 언어에 비해 강력한 기능을 수행할 수 있다.

주소에 직접 접근해서 데이터를 가져오는 등의 일을 할 수 있기 때문이다.

자바나 파이썬 등 기타 언어에서는 불가능한 일이다.

이렇게 강력한 기능을 가지고 있는 장점도 있는가 하면 단점도 있다.

포인터만 있으면 그 주소에 접근해서 그 주소에 있는 값을 읽어올 수 있다.

이것이 바로 강력한 기능이기도 하면서 치명적인 오류를 발생할 수 있는 부분이다.

잘못하면 중요한 데이터가 변경될 수도 있다.

 

예를 들어 이런 코드를 작성했다고 하자.

#include <iostream>
using namespace std;

int main()
{
	int* p;
	{	int a;
		a = 2;
		p = &a;
		cout << *p << endl;
	}
	cout << *p << endl; //허상 참조 발생
	return 0;
}

변수 a의 범위(scope)는 변수 a가 선언된 블럭 안에서만이다.

a가 선언된 그 중괄호를 벗어나면 a는 메모리의 스택에서 사라진다. 그리고 메모리의 그 공간을 안 쓰는 공간이라고 생각하게 된다.

따라서 메모리의 그 위치에 다른 데이터를 쓸 수도 있다.

그런데 포인터 p는 a보다 한 블럭 바깥 블럭에서 선언되었다.

(당연히 선언과 초기화를 따로 할 수도 있다.)

포인터 변수도 일반 변수와 마찬가지로

int* pa = &a;

이렇게 한 줄로 선언과 초기화를 동시에 할 수도 있는거고

int* pa;
pa = &a;

이렇게 선언과 초기화를 따로 두 문장으로 할 수도 있는 것이다.

 

아무튼 p가 존재하는 범위는 a보다 범위가 한 블럭 바깥 범위이다. 따라서 중괄호가 끝나 a는 메모리에서 사라졌지만 p는 아직도 살아있고 a가 저장되어 있었던 그 주소를 아직도 가리키고 있다. 

이렇게 a의 scope은 끝났지만 p의 scope은 끝나지 않았을 때 p가 간접참조연산자 *을 이용해서 *p 이렇게 접근해서 출력하면 2가 아니라 다른 이상한 값을 출력할 수도 있다.

메모리의 그 주소에 마침 다른 데이터가 아직 써지지 않았다면 2를 출력할 것이고 아니라면 이상한 값을 출력할 것이다.

그런데 이렇게 읽어만 와서 출력하는는 것은 그나마 다행이지 만약에 *p = 100; 이런 코드를 a의 scope 바깥에서 쓴다면 정말 치명적인 오류가 발생할 수도 있다. 그 주소에 중요한 데이터가 있었을 지도 모르는데 그 데이터를 100으로 덮어쓰는 것이기 때문이다.

이렇게 scope이 끝난 변수에 접근하는 것을 허상 참조(dangling pointer)라고 한다.

 

이처럼 포인터는 주소를 가지고 메모리의 그 위치에 접근할 수 있다는 강력한 기능을 가지고 있으면서도

그 장점으로 인한 치명적인 오류가 발생할 수도 있다.

 

그러한 오류를 막기 위해서 포인터를 이용한 코드를 짤 때에는 이러한 습관을 들이는 것이 좋다.

포인터 변수를 선언하고 바로 초기화해주지 않을 때에는 NULL로 초기화해주는 것이 좋다.

int* pa = NULL;

이렇게 NULL로 초기화를 해준다면 적어도 아까와 같은 치명적인 오류는 발생하지 않는다.

(참고로 NULL은 포인터 변수에만 사용할 수 있다.)

 

예를 들어 int* pa; 이렇게 선언해놓고 '나중에 pa에 a의 주소를 담아야지' 하고 생각하고 있었는데 깜빡하고 pa = &a; 이 코드를 쓰지 않고 pa에는 a의 주소가 있겠거니 하고 *pa 이렇게 역참조하면 아까와 같은 치명적인 오류가 발생할 것이다. pa를 초기화하지 않았기 때문에 pa가 선언된 메모리의 주소에 있던 쓰레기 값 32비트를 주소라고 생각하고 억지로 읽어와서 그 32비트 주소 메모리 위치에 접근해서 그 곳의 데이터를 바꿀 수가 있다.

 

하지만 NULL로 초기화해놨다면

널포인터라며 예외(런타임에러)가 발생한다. 이것은 치명적인 오류가 아니라 이것을 보고 '아 내가 널 포인터를 썼구나' 하고 코드를 짠 사람이 알게 되고 코드를 고칠 것이다.

따라서 처음에는 포인터를 NULL로 초기화 하는 것이 좋다.

+ Recent posts