C 언어에서는 malloc() 이나 free() 와 같은 표준 C 함수를 이용해 동적 메모리 할당 및 반환을 했습니다. 위 포스팅에 동적 메모리 할당과 반환은 무엇을 의미하는지 C 언어 기준으로 더 상세히 설명해놨습니다. C++ 에서는 new 와 delete 연산자를 통해 동적 메모리 할당/반환을 수행합니다.
new 연산자는 힙 으로부터 메모리를 할당 받고, delete 연산자는 할당 받은 메모리를 힙으로 반환합니다.
기본 자료형 동적 할당/반환
기본 형식은 다음과 같습니다.
데이터타입 *포인터변수 = new 데이터타입;
... // 사용이 끝난 후
delete 포인터 변수
new 연산자는 데이터타입의 크기만큼 힙으로부터 메모리를 할당받아 주소를 리턴합니다. 따라서 포인터 변수와 항상 세트로 붙어다닙니다. malloc 이랑 사용법이 동일하죠? 또한 이 때의 데이터 타입은 char, int, double 같은 기본 자료형 뿐만 아니라 구조체, 클래스도 포함됩니다. delete 연산자는 포인터 변수가 가리키는 메모리를 힙으로 반환합니다.
또한 힙 메모리가 부족한 경우 new 는 NULL 을 리턴하기 때문에 NULL 체크 또한 포함해주는 것이 좋습니다. malloc 을 사용할 때는 형변환도 해줘야했고, sizeof도 사용해야 했지만 new 는 그런 작업들을 안해줘도 됩니다.
int *p = new int;
if(!p) { return; } // NULL 이면 바로 return
*p = 10;
// ...
delete p;
// ------ 다양한 타입에 적용 가능 -------
int *pint = new int;
int *pchar = new char;
int *pCircle = new Circle();
delete pint;
delete pchar;
delete pCircle;
new 를 사용해 메모리를 할당 받을 때, 바로 초기값을 지정해서 초기화해줄 수 있습니다.
데이터타입 *포인터변수 = new 데이터타입(초깃값);
int *pInt = new int(20); // 20으로 초기화된 int 공간 할당
char *pChar = new char('a') // 'a'로 초기화된 char 공간 할당
배열 동적 할당/반환
배열을 동적 할당/반환할 때의 형식은 다음과 같습니다.
데이터타입 *포인터변수 = new 데이터타입 [배열의 크기];
delete[] 포인터변수;
할당 받는 new 형태는 뭔가 익숙한데, delete[] 형태는 좀 익숙치 않죠. 사용자로부터 몇 개의 정수를 입력 받을건지 먼저 입력받고 그에 맞는 배열을 동적 할당 받아 평균를 출력하고 다시 반환하는 예제 코드를 통해 살펴봅시다.
#include <iostream>
using namespace std;
int main()
{
int n, sum = 0;
cout << "입력할 정수 개수 : ";
cin >> n;
if (n <= 0) return 0; // 0이하 정수 입력하면 종료
int* p = new int[n]; // n개의 정수를 저장할 배열 동적 할당
if (!p) { cout << "메모리 동적 할당 오류"; return 0; }
for (int i = 0; i < n; i++) {
cin >> p[i];
sum += p[i];
// cin >> *(p+i);
// sum += *(p+i);
}
cout << "평균 = " << sum / n << endl;
delete[] p;
}
delete[] 를 안 쓰고 delete 로 초기화를 하면 오류가 발생하게 됩니다.
배열을 할당받자마자 초기화할 때는 생성자를 통해 직접 초기값을 지정할 수 없습니다.
int *ptr1 = new int[10](20) // 틀린 코드
int *ptr2 = new int(20)[10] // 틀린 코드
int *ptr3 = new int[] {1, 2, 3, 4} // 1, 2, 3, 4 가 저장된 배열이 생성된다. 옳은 초기화
객체 / 객체배열 동적 할당/반환
객체나 객체 배열의 동적 할당/반환은 사실 위랑 같습니다. 데이터타입 자리에 클래스이름 으로 대체될 뿐입니다.
클래스이름 *포인터변수 = new 클래스이름;
클래스이름 *포인터변수 = new 클래스이름(생성자매개변수리스트);
delete 포인터변수;
클래스이름 *포인터변수 = new 클래스이름[배열크기];
클래스이름 *포인터변수 = new 클래스이름[] { 생성자 하나씩 각각 써주면 초기화 가능 };
접근 방법 또한 포인터 사용법 완전 동일 !
내용이 동일하니 예제 코드를 위주로 살펴보겠습니다.
// 객체 동적 생성 및 반환 예시
#include <iostream>
using namespace std;
class Circle {
int radius;
public:
Circle() { radius = 1; cout << "생성자 실행 radius = " << radius << endl; }
Circle(int r) { radius = r; cout << "생성자 실행 radius = " << radius << endl; }
~Circle() { cout << "소멸자 실행 radius = " << radius << endl; }
void setRadius(int r) { radius = r; }
double getArea() { return 3.14 * radius * radius; }
};
int main() {
Circle* p, * q;
p = new Circle();
q = new Circle(30);
cout << "p가 가리키는 객체 면적 : " << p->getArea() << endl;
cout << "q가 가리키는 객체 면적 : " << q->getArea() << endl;
delete p;
delete q;
// 정수 값으로 반지름 입력 받고 Circle 객체를 동적 생성하여 면적을 출력. 음수 입력하면 종료
cout << endl << endl;
int r;
while (1) {
cout << "정수 반지름 입력(음수이면 종료) >> ";
cin >> r;
if (r < 0) break;
Circle* p = new Circle(r);
cout << "원의 면적 = " << p->getArea() << endl;
delete p;
}
}
원래 동적할당이 아닌 경우는 가장 나중에 생성된 객체가 가장 먼저 소멸됩니다. 그러나 delete 명령을 통해 우리가 순서를 부여하고 있기 때문에 먼저 실행된 반지름 1 객체가 먼저 소멸되는 것을 확인할 수 있습니다. 그 밑은 단순 응용 입니다.
다음으로 객체 배열을 동적 생성하는 아주 간단한 예를 보겠습니다.
#include <iostream>
using namespace std;
class Circle {
int radius;
public:
Circle() { radius = 1; cout << "생성자 실행 radius = " << radius << endl; }
Circle(int r) { radius = r; cout << "생성자 실행 radius = " << radius << endl; }
~Circle() { cout << "소멸자 실행 radius = " << radius << endl; }
void setRadius(int r) { radius = r; }
double getArea() { return 3.14 * radius * radius; }
};
int main() {
Circle* p = new Circle[3];
for (int i = 0; i < 3; i++) p[i].setRadius(10*(i+1));
Circle* ptr = p;
for (int i = 0; i < 3; i++) cout << (p + i)->getArea() << endl;
delete[] p;
}
동적 배열의 경우, 따로 설정해주지 않으면 각 객체마다 기본 생성자가 호출되는데, 따로 설정해주는 경우는 매개변수 있는 생성자가 호출됩니다. 배열의 경우 소멸자가 원래 순서대로 가장 나중에 생성된 객체가 가장 먼저 소멸하는 것을 확인할 수 있습니다. 그리고 만약 저기서 delete[] p; 가 아니라 delete p를 사용한다면?
이러한 오류가 발생하게 됩니다. 같은 메모리 공간에 대해서 반복적으로 delete 를 수행하거나 이렇게 잘못된 delete 연산자를 사용하는 경우 Expression: _CrtlsValidHeapPointer(block) 이라는 요류가 발생하게 됩니다. 이 오류가 발생한다면 C에서는 free 가 잘못되거나 이미 free 가 된 메모리에 free 를 한 경우가 많고, C++ 에서는 delete 를 했는데 또 하거나 배열인데 delete[] 연산자를 쓰지 않고 delete 연산자를 쓴 경우, 배열이 아닌데 delete[] 연산자를 쓴 경우가 많아서 이를 점검하면 대부분 해결됩니다.
번외로, 객체 배열을 생성하고 포인터 변수를 통해 접근하는 다양한 방법을 총 정리한 예시 코드입니다. 동적 생성/반환은 아니지만 객체나 객체 배열에 대한 포인터 변수의 접근을 다 써놨기 때문에 이 코드만 알아도 포인터는 사용할 줄 알게 됩니다.
#include <iostream>
using namespace std;
class Circle {
int radius;
public:
Circle() { radius = 1; cout << "생성자 실행 radius = " << radius << endl; }
Circle(int r) { radius = r; cout << "생성자 실행 radius = " << radius << endl; }
~Circle() { cout << "소멸자 실행 radius = " << radius << endl; }
void setRadius(int r) { radius = r; }
double getArea() { return 3.14 * radius * radius; }
};
int main()
{
Circle c[3] = { Circle(10), Circle(20), Circle() };
Circle* p = c;
cout << endl;
for (int i = 0; i < 3; i++) {
cout << "circleArray[] " << i << "의 면적은 " << c[i].getArea() << endl;
}
cout << endl;
for (int i = 0; i < 3; i++) {
cout << "*(circleArray+i) " << i << "의 면적은 " << (*(c + i)).getArea() << endl;
}
cout << endl;
for (int i = 0; i < 3; i++) {
cout << "p[i] " << i << "의 면적은 " << p[i].getArea() << endl;
}
cout << endl;
for (int i = 0; i < 3; i++) {
cout << "*(p+i) " << i << "의 면적은 " << (*(p + i)).getArea() << endl;
}
cout << endl;
for (int i = 0; i < 3; i++) {
cout << "p-> " << i << "의 면적은 " << (p + i)->getArea() << endl;
}
cout << endl << "c->getArea() = " << c->getArea() << endl;
cout << "(*(c + 1)).getArea() = " << (*(c + 1)).getArea() << endl << endl;
return 0;
}
'C++ > C++ 기초' 카테고리의 다른 글
[C++] 복사 생성자 (Copy constructor) - 얕은 복사 vs 깊은 복사 (0) | 2023.07.09 |
---|---|
[C++] 참조 매개 변수 / 참조 리턴 (Reference parameter / Reference return) + Call-by-reference (0) | 2023.07.09 |
[C++] 문자열 관련 함수 : string 클래스 정리 및 사용법 (0) | 2023.07.05 |
[C++] 기본 입출력 ( 화면 출력 / 키 입력받기 / 문자열 입력받기 ) (0) | 2023.07.02 |
[C++] 템플릿 ( Template ) - 함수 템플릿 / 클래스 템플릿 (0) | 2023.05.05 |