동적 메모리 할당 ▶ 변수를 선언하면 그 변수에 대한 기억공간이 메모리의 적절한 위치에 할당된다. 일반적으로 변수는 함수 내부에 선언되어 함수가 실행되는 동안만 존재하거나, 프로그램 시작과 함께 생성되어 프로그램을 종료할 때 소멸된다. 함수 내부에서 선언되는 지역변수들은 대부분 전자에 해당되며, 함수의 외부에 선언되는 전역변수나 함수 내부에서 static 키워드와 함께 선언되는 변수는 후자에 해당된다.
그런데 때에 따라서는 필요할 때 기억공간을 할당하고 더 이상 그 공간이 필요하지 않으면 반환할 수 있어야 한다. 이와 같은 기능을 동적 메모리 할당(dynamic memory allocation)이라고 한다. 그런데 동적 메모리 할당으로 생성된 저장공간은 이름이 없어 변수처럼 그 이름을 통해 액세스할 수 없다. 이러한 문제는 포인터를 이용하여 해결한다. 즉, 동적으로 할당된 저장공간을 포인터 변수가 가리키게 하면 그 포인터를 이용하여 액세스할 수 있다.
new와 delete ▶ 동적 메모리 할당은 다음과 같이 new 연산자를 이용한다.
1. ptrVar = new TypeName;
2. ptrVar = new TypeName[n];
형식1은 지정된 자료형의 데이터 1개를 저장할 수 있는 공간을 할당하여, 그 주소를 포인터 변수에 넣는다. 이때 자료형은 포인터의 자료형과 일치해야 한다. 형식2는 지정된 자료형의 데이터를 n개 저장할 수 있는 배열을 할당한다. n은 양의 정숫값을 내는 수식이면 되며, 상수가 아니어도 된다.
사용이 끝난 메모리 공간은 시스템에 반납하여 다른 용도로 할당해 사용할 수 있도록 한다. 이때에는 다음과 같이 delete 연산자를 사용한다.
1. delete ptrVar;
2. delete [] ptrVar;
delete 연산자는 new 연산자의 형식1로 할당한 메모리 공간을 반환할 때 사용하며, delete [] 연산자는 형식2의 방법으로 new 연산자를 사용하여 할당한 메모리 공간을 반환할 때 사용한다. 만일 ptrVar에 nullptr가 저장되어 있다면 delete는 아무 일도 하지 않는다.
int *intPtr; // int형 포인터의 선언
intPtr = new int; // int 값을 저장할 공간 할당
*intPtr = 10; // 할당된공간을 사용
.......
delete intPtr; // intPtr가 가리키는 공간 반환
intPtr = nullptr; // intPtr를 nullptr로 지정
위 예에서는 new 연산자로 int형 값을 저장할 공간 하나를 할당한다. 그 결과 시스템의 자유공간에서 4바이트가 할당되며, 그 주소가 intPtr에 저장된다(위의 메모리 동적 할당, 반환 그림 참조). 사용이 끝나고, 더 이상 그 공간이 필요하지 않으면 이를 시스템에 반환하여 필요할 때 그 공간을 재활용할 수 있게 한다. 이렇게 공간을 반환하더라도 intPtr는 여전히 그 공간을 가리키고 있다. 그러나 그 공간은 이미 반환된 것이므로, intPtr에 nullptr를 넣어 포인터가 가리키는 곳이 없음을 의미하도록 하는 것이 좋다.
위 그림은 다음과 같은 4개의 int 값을 저장할 수 있는 공간을 할당하는 것을 보여 준다.
int *intPtr; // int형 포인터의 선언
intPtr = new int[4]; // 4개의 int를 저장할 공간 할당
*intPtr = 10; // 할당된 공간을 사용
*(intPtr + 1) = 20;
intPtr[2] = 30; // *(intPtr + 2) = 30; 과 동일함
.....
delet [] intPtr; // intPtr가 가리키는 공간 반환
intPtr = nullptr; // intPtr를 nullptr로 지정
new int[4]는 int형 값 4개를 저장할 수 있는 배열을 할당하도록 지시하는 명령어이다. 이때 intPtr에는 할당된 공간의 첫 바이트의 주소가 전달된다. 나머지 3개의 저장공간은 intPtr로부터의 상대 위치를 지정하여 액세스할 수 있다. 사용 후 공간을 반납할 때에는 delete [] 연산자를 사용하는 것에 주의하자. 만일 new 연산자로 요청한 저장공간을 할당할 수 없을 때는 std::bad_alloc이라는 예외가 발생하며, C++의 예외처리 방식에 때라 대응한다. 만일 예외처리 방식을 원하지 않는다면 new 대신 new(nothrow)를 사용할 수 있는데, 이때 저장공간 할당에 실패하면 new 연산자는 nullptr를 내놓으며, 이를 검사하여 다음과 같은 형식으로 대응할 수 있다.
int *pt = new(nothrow) int[1000];
if(pt == nullptr){
..... // 할당 실패에 대응하는 처리를 함
}
포인터 연산 ▶ 할당된 배열 공간에서 각각의 저장공간을 액세스하려면 포인터 연산을 이용하여 기준 위치의 포인터 값으로부터의 상대 위치를 지정한다.
임의 포인터 ptr를 기준으로 첫째 값의 주소는 ptr, 둘째 값의ㅏ 주소는 ptr + 1, 셋째 값의 주소는 ptr + 2 등으로 표현한다. 또한 그 위치의 값은 각각 *ptr, *(ptr+1), *(ptr+2) 등으로 표현한다.
ptr, ptr+1, ptr+2 등의 실제 주소는 ptr가 어떤 자료형의 포인터인가에 따라 다르다. 예를 들어 위 그림의 (가)와 같이 100번지부터 시작하는 int형 데이터 블록을 가리키는 포인터 intPtr의 경우 int형이 4바이트를 차지하므로 intPtr가 가리키는 주소는 100, intPtr + 1은 104, intPtr + 2는 108번지가 된다. 반면 char형은 1바이트이므로 그림(나)에서 chPtr가 가리키는 주소는 100, chPtr + 1은 101, chPtr + 2는 102번지 등이 된다.
이와 같이 포인터에 대한 연산은 그 형에 따라 실제로는 다르게 구현된다. 이것은 일종의 연산자 다중정의에 해당된다. -, ++, -- 등의 연산도 동일한 개념에 따라 처리된다. C++에서는 포인터의 형에 대해 매우 엄격하다. 따라서
int *intPtr;
char *charPtr;
charPtr = intPrt; // 오류 - 포인터의 형이 다름
과 같이 다른 형의 포인터를 대입하는 것은 컴파일할 때 오류로 처리된다. 궅이 형 변환을 해야 할 경우는 reinterpret_cast를 사용해야 한다. static_cast는 사용할 수 없다.
다음은 포인터 ptr에 대한 연산들을 정리해 본 것이다.
연산 | 처리 |
ptr + n | ptr의 n번째 뒤의 저장공간에 대한 포인터 |
ptr - n | ptr의 n번째 앞의 저장공간에 대한 포인터 |
ptr++ | ptr가 현재 위치의 다음 값을 가리키도록 한다 |
ptr-- | ptr가 현재 위치의 앞의 값을 가리키도록 한다 |
*(ptr + n) 및 ptr[n] | ptr로부터 n번째 뒤에 저장된 값 |
*(ptr - n) 및 ptr[-n] | ptr로부터 n번째 앞에 저장된 값 |
*ptr++ 및 *ptr-- | *ptr의 값을 현재 수식에 사용하고, ptr가 다음 또는 이전 값을 가리키도록 한다. |
*++ptr 및 *--ptr | 먼저 ptr가 다음 또는 이전 값을 가리키도록 한 후, 그 위치의 값을 수식에 사용한다. |
(*ptr)++ 및 (*ptr)-- | ptr가 가리키는 곳의 값을 현재 수식에 사용한 후 그곳의 값을 1 증가 또는 감소시킨다. |
++(*ptr) 및 --(*ptr) | ptr가 가리키는 곳의 값을 1 증가 또는 감소시킨 후 그 값을 현재 수식에 사용한다. |
배열과 포인터 ▶ 배열과 포인터는 매우 밀접한 관계에 있다. 앞에서 배열의 이름은 배열의 첫 원소의 주소를 의미한다고 언급한 바 있다. 이는 포인터의 개녕과 거의 같다. 물론 배열변수는 포인터 변수가 아니므로 배열 이름이 가리키는 주소를 인위적으로 바꿀 수는 없다.
ArrayPtr.cpp는 배열과 포인터를 함께 사용하는 예를 보여 준다.
#include <iostream>
using namespace std;
int main() {
char str[14] = "Hello, world!";
char* pt;
cout << str << endl;
pt = str; // pt기 배열 str를 가리킴
while (*pt) { // 문자열의 끝이 아니면 반복
if (*pt >= 'a' && *pt <= 'z') // 소문자인 경우
*pt = *pt - 'a' + 'A'; // 대문자로 바꿈
pt++; // 다음 문자로 포인터 이동
}
cout << str << endl;
return 0;
}
====== 결과 ======
Hello, world!
HELLO, WORLD!
char str[14]는 str에 "Hello, world!"라는 문자열을 저장하기 위한 문자 배열이다. 이 문자열에는 13개의 문자만 보여 문자열의 길이는 13이지만, 실제로는 문자열의 끝을 알 수 있도록 널문자('/\0', 문자코드 0)가 끝에 포함되어 있다. 그러므로 총 14개의 문자를 저장할 공간이 필요하다. 이러한 문자열 표현 방식은 C언어 이후 계속 사용되어 오던 것으로, 흔히 C 스타일 문자열이라고 한다.
pt = str은 배열 str의 시작 주소를 나타내므로 포인터 pt는 배열의 시작 위치를 가리킨다. while문은 pt가 가리키고 있는 곳의 값이 0이 아니면, 즉 문자열의 끝이 아니면 반복 실행할 것을 지시하고 있다.
'Programming Languages > C++' 카테고리의 다른 글
[함수]-함수의 정의와 호출-함수의 정의 (0) | 2020.05.12 |
---|---|
참조 (0) | 2020.05.11 |
포인터 (0) | 2020.05.06 |
배열 (0) | 2020.04.30 |
클래스 (0) | 2020.04.29 |