이번에는 개발자가 직접 힙 메모리(heap memory) 구역에 메모리를 동적으로(dynamic) 할당(allocate)하거나 해제(deallocate)하는 연산자인 new와 delete에 대해서 알아보자. 앞에서 Raw pointer에 대해서 공부하면서 기본적인 것은 다루고 갔지만, 그래도 조금 더 자세히 알아보자
new와 delete 연산자의 기본 사용법은 다음과 같다.
data_type *pointer = new data_type; delete pointer;new 연산자는 자료형의 크기만큼 힙으로부터 메모리를 할당받고, 해당 메모리 주소를 리턴한다. 반대로, delete 연산자는 포인터 변수가 가리키는 메모리를 힙으로 다시 반환하게 된다.
💡 자료형
여기서 자료형은 단순 정수형, 문자 등 뿐만 아니라 구조체, 클래스 등을 포함할 수 있다.
💡 만약에 힙 메모리의 공간이 부족하다면 NULL을 리턴하기 때문에 new 연산자를 통해서 메모리를 할당한 이후에는 해당 메모리가 NULL인지 아닌지 검사를 해보는 것이 좋다.
int *p = new int; if(!p){ return; // 메모리 할당 실패 } *p = 5; int n = *p; delete p;
동적 메모리 할당을 수행할 때는 단순히 선언 및 할당 뿐만 아니라, 다음과 같은 방식으로 초기화도 가능하다.
💡 동적 할당의 초기화
data_type *ptr = new data_type(initial_value);
그리고 delete 연산자를 사용할 때 적절하지 않은 포인터를 반환하거나, 혹은 이미 반환한 포인터를 다시 반환하는 경우 실행 오류(runtime error)가 발생한다.
new와 delete 연산자를 통해서 배열 역시 동적으로 할당하고 해제할 수 있다.
data_type *ptr = new data_type[array_size];
delete[] ptr;
new 연산자를 통해서 배열의 크기만한 메모리를 할당받을 수 있으며, delete[] 연산자를 통해서 할당받은 배열 메모리를 반환하게 된다.
동적으로 할당받은 배열이라고 하더라도, 일반 배열과 사용법에 있어서 차이점이 없다. 그냥 인덱스(index)를 통해서 자유롭게 사용하면 된다.
다음은 배열의 동적 할당을 다루는 예시 코드이다.
#include<iostream>
using namespace std;
int main() {
cout << "입력할 정수의 개수는? ";
int n;
cin >> n;
if (n <= 0) return 0;
int* p = new int[n]; // n개의 정수 배열 동적 할당
if (!p) {
cout << "메모리를 할당할 수 없습니다." << endl;
return 0;
}
for (int i = 0; i < n; i++) {
cout << i + 1 << "번째 정수: ";
cin >> p[i];
}
int sum = 0;
for (int i = 0; i < n; i++) {
sum += p[i];
}
cout << "평균 = " << sum / n << endl;
delete[] p;
}
📌 배열의 동적 할당에서 주의해야 하는 점
1. 생성자를 통해 직접 초기화를 시키고, 초깃값을 부여할 수 없다.int *pArray = new int[10](20); // 오류 발생
- 하지만 선언과 구현을 동시에 함으로써 값을 부여할 수 있다.
int *pArray = new int[] {1,2,3,4}; // 1,2,3,4로 초기화 된 배열 변수 생성
- delete 할 때 반드시 []를 붙여야 함에 주의하자.
일반 변수와 마찬가지로 객체도 동적으로 생성하고 반환할 수 있다.
class_name *ptr = new class_name; // 기본 생성자 사용
class_name *ptr = new class_name(param_list); // 매개변수가 존재하는 생성자 사용
그리고 마찬가지로 delete 연산자를 이용해서 객체에 할당된 메모를 반환한다.
거듭 강조하지만, delete 연산자를 사용할 경우 포인터 변수는 반드시 new를 이용하여 동적으로 할당받은 메모리의 주소여야 한다. 동적으로 할당받지 않은 포인터 변수는 delete를 통해서 반환이 불가하다.
예시 코드를 2개 살펴보자.
#include <iostream>
using namespace std;
class Circle {
int radius;
public:
Circle();
Circle(int r);
~Circle();
void setRadius(int r) { radius = r; }
double getArea() { return 3.14 * radius * radius; }
};
Circle::Circle() {
radius = 1;
cout << "생성자 실행 : " << radius << endl;
}
Circle::Circle(int r) {
radius = r;
cout << "생성자 실행 : " << radius << endl;
}
Circle::~Circle() {
cout << "소멸자 실행 " << radius << endl;
}
int main() {
Circle* p, * q;
p = new Circle;
q = new Circle(30);
cout << p->getArea() << endl << q->getArea() << endl;
delete p;
delete q;
}
또 다른 예시이다.
#include <iostream>
using namespace std;
class Circle {
int radius;
public:
Circle();
Circle(int r);
~Circle();
void setRadius(int r) { radius = r; }
double getArea() { return 3.14 * radius * radius; }
};
Circle::Circle() {
radius = 1;
cout << "생성자 실행 : " << radius << endl;
}
Circle::Circle(int r) {
radius = r;
cout << "생성자 실행 : " << radius << endl;
}
Circle::~Circle() {
cout << "소멸자 실행 " << radius << endl;
}
int main() {
int radius;
while (true) {
cout << "정수 반지름 입력(음수면 종료)>>";
cin >> radius;
if (radius < 0) break;
Circle* p = new Circle(radius);
cout << "원의 면적은 " << p->getArea() << endl;
delete p;
}
}
또한 객체에 대해서도 객체 배열을 동적으로 할당하고 해제할 수 있다.
class_name ptr = new class_name[array_size];
일반적인 배열을 동적으로 할당할 때도 마찬가지로, 기본 생성자를 제외한 생성자를 직접적으로 호출할 수 없다. 하지만 선언과 동시에 구현한다면, 기본 생성자가 아닌 생성자를 불러와서 초기화 할 수 없다.
Circle *pArray = new Circle[3](30); // 컴파일 오류 발생 Circle *pArray = new Circle[3] { Circle(1), Circle(2), Circle(3)}; // 이 경우에는 오류가 발생하지 않고 초기화 된 값으로 생성이 가능
동적으로 생성된 객체 배열 역시 일반적인 객체 배열과 완전히 동일하게 사용할 수 있다. 그리고 delete[] 연산자를 통해서 배열을 반환하는 것까지 동일하다. 이 경우 소멸자의 호출 순서는 생성의 반대인 것도 동일하다.
예시 코드를 하나 살펴보자.
#include <iostream>
using namespace std;
class Circle {
int radius;
public:
Circle();
Circle(int r);
~Circle();
void setRadius(int r) { radius = r; }
double getArea() { return 3.14 * radius * radius; }
};
Circle::Circle() {
radius = 1;
cout << "생성자 실행 : " << radius << endl;
}
Circle::Circle(int r) {
radius = r;
cout << "생성자 실행 : " << radius << endl;
}
Circle::~Circle() {
cout << "소멸자 실행 " << radius << endl;
}
int main() {
Circle* pArray = new Circle[3];
pArray[0].setRadius(10);
pArray[1].setRadius(20);
pArray[2].setRadius(30);
for (int i = 0; i < 3; i++)
cout << pArray[i].getArea() << endl;
Circle* p = pArray;
for (int i = 0; i < 3; i++) {
cout << p->getArea() << endl;
p++;
}
delete[] pArray;
}
📌 주의점 !!!
힙 메모리에서 할당한 메모리는 반드시 반환해야 한다.
반환하지도 않고 사용하지도 않는 메모리가 되는 것을 메모리 누수(memory leakage)가 되버리며, 프로그램에서 사용할 수 있는 힙 영역의 메모리가 감소해 결국에는 사용하지 못하게 될 수 있다.
📌 안심할만한 점 !!
다만, 프로그램을 종료할 경우 힙 전체가 운영체제에 의해 자동 반환되기 때문에 프로그램 종료 이후에는 메모리 누수에 걱정할 필요 없다. 프로그램이 돌아가는 와중에 문제가 생길 뿐이다.
💡 this 포인터
this는 객체 자신에 대한 포인터(pointer)이다. 클래스의 멤버 함수 내에서만 사용된다.
💡 this 포인터의 정의
this는 전역 변수도 아니고, 함수 내에 선언된 지역 변수도 아니며, 객체 멤버 함수가 호출될 때 컴파일러에 의해서 보이지 않게 전달되는 객체에 대한 주소이다.class Circle { int radius; public: Circle() { this->radius = 1; } Circle(int r) {this->radius = radius; } void setRadius(int radius) { this->radius = radius; } ... };
💡 this란 무엇일까?
thiw는 컴파일러에 의해서 탄생하는 변수이다.
예를 들어서, 다음과 같은 코드가 있다고 가정하자.class Sample{ int a; public: void setA(int x){ this->a = x; } };여기에서 this가 선언된 부분도, this를 선언한 헤더 파일도 찾아볼 수 없다. 하지만 컴파일 오류가 발생하지 않는데, 이는 컴파일러가 this를 다음 코드와 같이 변환해서 다루기 때문이다.
class Sample{ int a; public: void setA(Sample *this, int x){ this->a = x; } };this가 위치한 곳에, Smaple* this로 변환하는 행위는 this를 사용하는 모든 멤버 함수에게 이루어진다. 이제 this는 setA()가 호출되면 생성되고, 종료되면 사라지는 매개변수임을 확인할 수 있다.
💡 1번쨰 : 멤버 변수의 이름과 동일한 이름으로 매개 변수의 이름을 정하고자 할 때 this가 필요하다. 예를 들어보자.
Circle(int radius){ this->radius = radius; }📌 2번째 : 객체 자기 자신의 주소를 리턴할 때. 이 때 this는 반드시 필요하다. 마찬가지고 예를 들어보자.
class Sample { public: Sample *f(){ ... return this; // 객체 자기 자신의 주소 리턴 } };연산자 중복을 구현할 때 이런 경우가 많이 발생하며, 이럴 때 this가 반드시 필요한 시점들이 있다.
다만, this를 사용하는 것이 만능도 아니며, 여러가지 제약 조건도 가지고 있음을 명심해야 한다.
💡 this의 제약 조건
1. 클래스의 멤버 함수에서만 사용할 수 있다.
2. static으로 선언된 멤버 함수에서는 사용할 수 없다. static 멤버 함수는 객체가 생성되기 전에 호출될 수 있으며, 또 호출되어 실행되는 시점에 객체가 존재하지 않을 수 있기 때문이다.