객체지향 프로그래밍에서 가장 중요한 것 중 하나는 메모리를 할당하고 관리하는 능력입니다. 특히 객체가 복사될 때 내부 데이터(특히 포인터)가 어떻게 처리되는지 이해하는 것이 핵심입니다.
// 예시
int a(5);
int a = int(5);
Runtime)에 필요한 만큼 메모리를 요청합니다.delete를 통해 직접 메모리를 해제해주지 않으면 메모리 누수(Memory Leak)가 발생합니다. // 예시 (new 연산자 활용)
int *a = new int(5);
delete a; // 반드시 해제!
new int(5)선언을 하면
1. int를 담는 공간 하나가 동적으로 생성
2. 5라는 값이 들어간다.
3. 접근하기 위해서 새로 만들어진 공간의 주소값을 반환(포인터 변수 선언)
객체를 동적으로 생성하면 new를 하는 순간 생성자가 호출되고, delete를 하는 순간 소멸자가 즉시 호출됩니다. 반면 일반 객체는 함수가 끝날 때 역순으로 소멸합니다.
class Vector {
public:
Vector() : x(0), y(0) {
cout << this << " : Vector()" << endl;
}
Vector(float x, float y) : x(x), y(y) {
cout << this << " : Vector(float, float)" << endl;
}
// 소멸자
~Vector() {
cout << this << " : ~Vector()" << endl;
}
float getX() {return x;}
float getY() { return y; }
private:
float x, y;
};
int main(void) {
cout << "main() 함수 시작" << endl;
// 정적 할당 객체
Vector v1;
Vector v2(3, 4);
//동적 할당
Vector* v3 = new Vector();
Vector* v4 = new Vector(5, 6);
cout << "(" << v3->getX() << "," << v3->getY() <<")"<< endl;
cout << "(" << v4->getX() << "," << v4->getY() << ")" << endl;
delete v3; // v3 즉시 소멸
delete v4;
cout << "main() 함수 끝" << endl;
}// 함수 종료 시 v2, v1 소멸
//출력 과정
this는 주소를 출력
main() 함수 시작
0x...10 : Vector() // v1 생성
0x...20 : Vector(float, float) // v2 생성
0x...30 : Vector() // v3 생성 (new)
0x...40 : Vector(float, float) // v4 생성 (new)
(0,0) // v3->getX(), getY()
(5,6) // v4->getX(), getY()
0x...30 : ~Vector() // delete v3 실행 (즉시 소멸)
0x...40 : ~Vector() // delete v4 실행 (즉시 소멸)
main() 함수 끝
0x...20 : ~Vector() // v2 소멸 (main이 끝나며 역순 소멸)
0x...10 : ~Vector() // v1 소멸 (main이 끝나며 역순 소멸)
메모리를 복사할 때, 특히 포인터 변수가 포함된 경우 복사의 방식이 매우 중요합니다.
delete 시 이미 해제된 메모리를 또 해제하려는 에러가 발생할 수 있습니다. int *a = new int(3)
//실제 int를 담는 공간과 그 공간에 있는 값이 복사 된 것이 아니라, 주소값만 복사
int *b = a;
//결과
a 값 1000(주소)
b 값 1000(주소)
1000의 주소에 있는 값 3
*a=5;
//결과
a 값 1000(주소)
b 값 1000(주소)
1000의 주소에 있는 값 3 -> 5
int *a = new int(3)
int *b = new int(*a)
//결과
a 값 1000(주소), 1000의 주소값 3
b 값 2000(주소), 2000의 주소값 3
*a=5;
//결과
a 값 1000(주소), 1000의 주소값 5
b 값 2000(주소), 2000의 주소값 3
클래스 멤버로 포인터가 있을 때, 기본 복사를 수행하면 어떤 일이 벌어지는지 분석해봅시다.
class A{
public:
A(int x, int y) : x(x), y(y) {
int x;
int *y;
};
int main(void({
A a(1, new int(2));
A b = a; 기본 복사 발생 (얕은 복사)
a.x = 3; // a.x는 일반 변수라 b.x에 영향 없음 (값 복사)
*a.y = 4; // a.y는 포인터라 주소가 복사됨. b.y도 똑같이 4로 변함 (얕은 복사)
cout << b.x << endl; // 출력: 1
cout <<*b.y << endl; // 출력: 4
delete a.y; // a.y를 해제하면 b.y는 갈 곳 잃은 포인터(Dangling Pointer)가 됨
}
//출력 결과
1. A a(1, new int(2));
a안에, x, y가 있는데,
x의 값은 1
y의 값은 1000(주소값), *y(1000의 주소에는)의 값은 2
2. A b = a;
b안에, x, y가 있는데,
x의 값은 1
y의 값은 1000(주소값), *y(1000의 주소에는)의 값은 2 (얕은 복사)
3. a.x = 3;
a안에, x, y가 있는데,
x의 값은 1 -> 3
y의 값은 1000(주소값), *y(1000의 주소에는)의 값은 2
b안에, x, y가 있는데,
x의 값은 1 (변화 없음, 값 복사이기 때문에(call by value))
y의 값은 1000(주소값), *y(1000의 주소에는)의 값은 2
4. *a.y = 4;
a안에, x, y가 있는데,
x의 값은 3
y의 값은 1000(주소값), *y(1000의 주소에는)의 값은 2 -> 4
b안에, x, y가 있는데,
x의 값은 1
y의 값은 1000(주소값), *y(1000의 주소에는)의 값은 2 -> 4 (얕은 복사)
요약:
1. new를 썼다면 반드시 delete를 짝지어주자.
2. 포인터를 멤버로 가진 클래스라면, 주소만 옮기는 얕은 복사가 일어날 수 있음을 항상 경계하자!