[C++] - 3일차(동적 할당)

Jun·2026년 2월 17일

c++

목록 보기
3/6

동적 할당과 메모리의 관리 (얕은 복사 vs 깊은 복사)

객체지향 프로그래밍에서 가장 중요한 것 중 하나는 메모리를 할당하고 관리하는 능력입니다. 특히 객체가 복사될 때 내부 데이터(특히 포인터)가 어떻게 처리되는지 이해하는 것이 핵심입니다.


1. 정적 할당 vs 동적 할당

정적 할당 (Static Allocation)

  • 특징: 컴파일 시점에 메모리 크기가 결정됩니다.
  • 장점: 사용하기 쉽고 관리(해제)를 신경 쓸 필요가 없습니다.
  • 단점: 실행 도중에 메모리 크기를 변경할 수 없어 효율성이 떨어질 수 있습니다.
    // 예시
    int a(5);
    int a = int(5);

동적 할당 (Dynamic Allocation)

  • 특징: 프로그램 실행 도중(Runtime)에 필요한 만큼 메모리를 요청합니다.
  • 장점: 메모리를 효율적으로 사용할 수 있고, 필요한 시점에 생성/소멸이 가능합니다.
  • 단점: delete를 통해 직접 메모리를 해제해주지 않으면 메모리 누수(Memory Leak)가 발생합니다.
    // 예시 (new 연산자 활용)
    int *a = new int(5); 
    delete a;            // 반드시 해제!

new int(5)선언을 하면
1. int를 담는 공간 하나가 동적으로 생성
2. 5라는 값이 들어간다.
3. 접근하기 위해서 새로 만들어진 공간의 주소값을 반환(포인터 변수 선언)


2. 객체의 동적 할당과 소멸 순서

객체를 동적으로 생성하면 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이 끝나며 역순 소멸)


3. 얕은 복사(Shallow Copy) vs 깊은 복사(Deep Copy)

메모리를 복사할 때, 특히 포인터 변수가 포함된 경우 복사의 방식이 매우 중요합니다.

얕은 복사 (Shallow Copy)

  • 정의: 주소값만 복사하는 방식입니다.
  • 문제점: 두 포인터가 같은 메모리 공간을 가리키게 되어, 한쪽에서 값을 바꾸면 다른 쪽도 영향을 받습니다. 또한 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

깊은 복사 (Deep Copy)

  • 정의: 실제 데이터가 담긴 새로운 공간을 따로 만들어서 내용물만 복사하는 방식입니다.
  • 장점: 두 객체는 완전히 독립된 메모리를 가집니다.
    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

4. 포인터 멤버를 가진 클래스의 복사 예시

클래스 멤버로 포인터가 있을 때, 기본 복사를 수행하면 어떤 일이 벌어지는지 분석해봅시다.

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. 포인터를 멤버로 가진 클래스라면, 주소만 옮기는 얕은 복사가 일어날 수 있음을 항상 경계하자!

profile
Hard Trying

0개의 댓글