18.포인터와 메모리 해제
C의 꽃은 포인터라고 한다. 포인터가 이해가 안 될 땐 이걸 왜 사용해야 하는지도 몰랐으나, 현재는 포인터 없이 어떻게 개발하는거지 란 생각이 든다. 포인터는 값 자체가 아니라 값의 주소를 저장하는 변수이다. 포인터는 별표(*)를 사용한다.
포인터가 값의 주소를 나타낸다면, 변수의 주소는 어떻게 알 수 있을까? 답은 주소 연산자(&)를 변수 앞에 붙이면 된다. 주소 연산자는 레퍼런스(reference)라고도 부른다.
교과서적인 포인터는 아래와 같다.
int main()
{
using namespace std;
int a = 5;
int* b = &a;
cout << "a의 주소 : " << &a << endl;
cout << "b의 주소 : "<<b << endl;
cout << "a의 값 : " << a << endl;
cout << "b의 값 : "<<*b << endl;
return 0;
}
변수 a는 정수형이고, b는 정수형 변수의 주소이다. &a는 주소를 나타내므로 b에 대입할 수 있다.
쉽게 생각해서 int*형은 주소를 갖고 있는데, 그 주소로 들어가면 정수형인 변수라는 의미이다.
포인터의 위험
포인터를 사용한다는 것은 컴퓨터의 메모리 주소를 사용한다는 의미이다. 다만, 컴퓨터의 메모리 주소는 사람의 눈에 보이지 않는다. 즉, 메모리 주소를 직접적으로 찾아가는 것은 힘들다는 것이다.
long* fellow;
*fellow = 223323;
위 코드의 경우 fellow가 가리키는 메모리 주소에는 223323이 저장 될 것이다. 하지만 fellow는 초기화하지 않았으므로 쓰레기 값이 있을 것이다. 즉, 223323의 주소는 쓰레기 값이므로 알 방법이 없다.
포인터와 수
일반적인 컴퓨터의 메모리 주소는 정수형으로 다루고 있지만, 포인터에 정수를 직접 대입할 수 없다.
int* ptr;
ptr = 0xB8000000; //불가
ptr = (int*)0xB8000000; //문제 없음
new를 사용한 메모리 대입
포인터의 진정한 가치는 런타임 도중에 이름이 없는 메모리를 대입하는 것이다. 이때 C++에서는 new 연산자를 사용한다. 동적 할당(dynamic allocation)이라고도 한다.
int* pn = new int;
new int 부분은 int형 데이터를 저장할 새로운 메모리가 필요하다고 프로그램에 알린다. new 연산자는 뒤따르는 데이터형을 보고, 몇 바이트가 필요한지 파악한 후, 적당한 메모리를 찾아 필요한 만큼 블록을 대입하고, 그 주소를 리턴한다. 리턴된 주소를 pn에 대입한다.
아래의 코드는 new 연산자를 사용한 포인터 선언 코드이다.
int main()
{
using namespace std;
int* pn = new int; //메모리 할당
*pn = 100; //pn의 주소안의 값 대입
cout << pn << endl;
cout << *pn << endl;
delete pn; //메모리 해제
system("Pause");
return 0;
}
delete를 사용한 메모리 해제
.new 연산자를 사용했다면, delete 연산자는 잊지 말아야한다. new 연산자로 메모리 주소를 할당 했다면, delete 연산자는 메모리 주소 반환이다.
delete pn;
만약 delete 연산자를 사용하지 않는다면, 메모리 누수가 발생한다. 메모리 누수가 발생한다는 것은 쓸데 없는 것이 메모리를 차지하고 있어서 사용할 수 없다는 것이다. 이 문제는 프로그램을 꺼도 유지되며, 컴퓨터를 종료하기 전까지 계속 유지된다.
new를 사용한 동적 배열의 생성
배열, 문자열, 구조체와 같은 큰 데이터를 다룰 때에는 new를 사용하는 것이 효율이 좋다. 배열의 경우 선언 할 때 배열의 크기가 정해지고, 컴파일 시 배열의 메모리가 할당된다. 할당된 메모리는 사용이 되든 안 되든 항상 차지하고 있따. 이 방식을 정적 바인딩(static binding)이라고 한다. 하지만 new 연산자를 사용한다면 배열을 런타임 중에 생성할 수 있고, 필요 없으면 생성하지 않을 수도 있다. 또한 런타임 중에 배열의 크기를 결정할 수 있다. 이 방식을 동적 바인딩(dynamic binding)이라고 한다.
new를 사용한 동적 배열의 생성
배열 원소의 데이터형과 개수를 new에 알려주면 된다.
int* psome = new int[10];
new 연산자는 배열의 첫 번째 원소의 주소를 리턴한다. 위 코드에서 psome이 가리키는 주소는 배열의 첫 번째 주소라는 말이다.
new를 사용하여 생성된 배열을 메모리 해제할 땐, 아래와 같이 하면 된다.
delete[] psome;
동적 배열의 사용
동적 할당한 배열의 원소의 값에 접근하는 방식은 일반적인 포인터 변수와 다르다. 일반적인 포인터 변수의 값에 접근하는 경우 *을 붙인다.
int a = 5;
int* b = &a;
std::cout << *b << std::endl;
하지만 배열의 경우 *을 붙이지 않아도 된다. 즉, 첫 번째 원소는 *psome[0]이 아닌, psome[0]으로 접근할 수 있고, 두 번째 원소는 psome[1]으로 접근할 수 있다. 이런 방법이 가능한 이유는 C++가 배열을 구현할 때 내부적으로 포인터를 사용하기 때문이다. 또한, psome이 가리키는 주소는 배열의 첫 번째 주소라는 점을 기억해야 한다. 이 말은 *psome은 psome[0]과 같다.