포인터와 배열의 관계
일반 배열을 나타낼 때에는 다음과 같이 작성합니다.
int ary[5] = { 1, 2, 3, 4, 5 }; // 일반 배열
이때 배열 변수의 이름, 즉 ary는 ary의 첫번째 요소의 주소를 가진 포인터와 같습니다. 따라서 다음과 같은 결과가 나타납니다.
int ary[5] = { 1, 2, 3, 4, 5 };
printf("%d", *ary); // 1을 출력합니다
포인터는 +/- 연산이 가능한데, 일반적인 +/-연산과 다르게 포인터의 +/- 연산은 데이터 형식의 크기를 기준으로 계산합니다.
char chs[5] = { 'A', 'B', 'C', 'D', 'E' };
printf("%p / %p\n", chs, (chs + 1));
// chs 주소와 chs+1 주소의 차이는 char의 크기인 1만큼 차이가 납니다.
int nums[5] = { 1, 2, 3, 4, 5 };
printf("%p / %p\n", nums, (nums + 1));
// nums 주소와 nums+1 주소의 차이는 int의 크기인 4만큼 차이가 납니다.
따라서 배열 데이터를 순회할 때 다음과 같은 방법으로도 순회할 수 있습니다.
int nums[5] = { 1, 2, 3, 4, 5 };
for (int i = 0; i < 5; i++)
{
printf("%d\n", *(nums + i));
}
포인터 배열
일반 배열을 선언하면 컴파일 타임에서 배열의 길이가 고정된, 정적할당된 배열을 선언하게 되어 오버플로우 또는 메모리 낭비와 같은 문제를 초래할 가능성이 있습니다.
일반 배열의 길이를 컴파일 타임에 정해야 하는 이유는 시스템에서 프로그램에 실행할 때 필요한 메모리를 미리 확보하고 실행하기 위함입니다. 조금 자세히 말하자면, Stack 메모리를 사용하기 때문에 이 메모리의 크기는 컴파일 타임에 결정됩니다.
이를 해결하기 위해서 런타임에서 배열의 길이를 정하고 선언하는 배열, 동적할당된 배열을 사용합니다. 동적할당된 데이터는 Heap 메모리를 사용하기 때문에 런타임에 데이터를 할당 및 해제할 수 있습니다. 포인터 배열을 선언하고 동적할당하기 위해서는 new와 delete 키워드를 사용합니다.
new 키워드
new는 데이터 공간을 할당하고 초기화하는 키워드입니다. 포인트 배열을 선언할 때 new 키워드는 다음과 같이 사용합니다.
자료형* 배열이름 = new 자료형[배열크기] { 초기값1, ... , 초기값n }; // 1차원 포인터 배열
이렇게 선언된 포인터 배열은 런타임에 할당되어 가변적인 데이터 배열을 사용할 수 있습니다.
int size = 5;
int nums1[size] = { 1, 2, 3, 4, 5 }; // 오류 발생!
int* nums2 = new int[size] { 1, 2, 3, 4, 5 };
일반 배열에서는 배열의 크기를 컴파일 타임에 정해야 하기 때문에 런타임에 생성된 size 변수를 사용하여 배열을 선언할 수 없지만, 포인터 배열은 동적할당을 통해서 런타임에 생성된 size 변수를 사용하여 배열을 선언할 수 있습니다.
int** nums = new int*[5]
{
new int[5] {1, 2, 3, 4, 5},
new int[5] {1, 2, 3, 4, 5},
new int[5] {1, 2, 3, 4, 5},
new int[5] {1, 2, 3, 4, 5},
new int[5] {1, 2, 3, 4, 5},
};
2차원 포인터 배열에 데이터를 동적할당할 때는 위와 같이 작성합니다.
delete 키워드
delete는 new 키워드로 할당된 데이터 공간을 반납하는 키워드입니다.
new로 선언된 데이터는 사용후 반드시 delete로 반납하는 것이 원칙이며, 특히 규모가 큰 프로그램에서 이를 행하지 않으면 데이터 누수가 발생하고 이는 곧 성능에 영향을 미칩니다.
1차원 포인트 배열을 new로 선언한 후 데이터 공간을 반납할 때 delete 키워드는 다음과 같이 사용합니다.
자료형* 배열이름 = new 자료형[배열크기];
// ...
delete[] 배열이름;
int* nums = new int[5] { 1, 2, 3, 4, 5 };
// ...
delete nums;
// nums 계속 사용 가능?
// nums += 1; ...
위 코드는 nums 포인터 배열 변수를 선언, 할당하고 해제하는 과정을 나타낸 코드입니다. 하지만 nums 데이터는 3번째 행에서 반납되어 논리적으로 사라진 상태이지만 3번째 행 이후에서도 접근할 수 있다는 문제가 있습니다.
#define SAFE_ARYPTR_DELETE(ptr)\
if (ptr)\
{\
delete[] ptr;\
ptr = nullptr;\
}
int* nums = new int[5] { 1, 2, 3, 4, 5 };
// ...
SAFE_ARYPTR_DELETE(nums);
반납된 데이터를 사용하지 않도록 사용자가 더 주의를 기울이는 것도 좋지만 위와 같은 새 코드를 추가하여 더 안전하게 데이터를 반납하는 방법도 있습니다.