# C언어 (9)

## 1\. new/delete 연산자와 동적 메모리

### 1-1. 동적 메모리의 필요성

동적 메모리는 **실행시간(런타임)에 할당되어 사용되는 메모리 블록** 이다. 프로그램이 돌아가는 도중에 필요한 만큼 메모리를 확보하고, 다 쓰면 반환하는 방식이다.

동적 메모리의 반대는 **정적 메모리** 다. 정적 메모리는 컴파일 타임에 크기가 결정되는 메모리로, 일반 변수나 배열이 여기에 해당한다. `int arr[100]`은 컴파일 시점에 400바이트가 필요하다는 것이 확정된다.

그런데 프로그램을 작성할 때 **얼마만큼의 메모리가 필요한지 알지 못하는 경우** 가 많다. 사용자가 데이터를 몇 개 입력할지, 파일에 몇 줄이 있는지, 네트워크로 얼마나 큰 데이터가 올지는 프로그램이 실행되어야 알 수 있다. 배열 크기를 넉넉하게 `int arr[10000]`으로 잡으면 대부분의 메모리가 낭비되고, 그마저도 10000개를 넘으면 부족해진다.

동적 메모리는 이 문제를 해결한다. 필요한 시점에 필요한 만큼만 할당하고, 다 쓰면 해제하면 된다.

동적 메모리가 할당되는 영역을 **힙(Heap) 영역** 이라 한다. C에서 `malloc`과 `free`로 힙을 관리했던 것처럼, C++에서는 `new`**로 생성하고** `delete`**로 소멸** 시킨다.

### 1-2. new와 delete

C에서는 `malloc`과 `free`를 사용했다. C++에서도 쓸 수는 있지만, C++에는 더 나은 방법이 있다.

```cpp
// C 방식
int *p = (int *)malloc(sizeof(int));
*p = 10;
free(p);

// C++ 방식
int *p = new int;
*p = 10;
delete p;
```

`new`는 `malloc`보다 간결하다. `sizeof`를 쓸 필요도 없고, 타입캐스팅도 필요 없다. `new int`라고 쓰면 `int` 크기만큼 힙에 메모리를 할당하고 그 주소를 반환한다.

선언과 동시에 초기화할 수도 있다.

```cpp
int *p = new int(42);      // 42로 초기화
double *d = new double(3.14);
```

배열을 동적으로 할당할 때는 `new[]`와 `delete[]`를 사용한다.

```cpp
int *arr = new int[5];     // int 5개 크기의 배열 할당

for (int i = 0; i < 5; i++) {
    arr[i] = (i + 1) * 10;
}

delete[] arr;  // 배열은 반드시 delete[]로 해제
```

`delete`와 `delete[]`를 혼동하면 안 된다. 단일 객체는 `delete`, 배열은 `delete[]`다. 배열에 `delete`를 쓰면 정의되지 않은 동작이 발생한다.

### 1-3. new/delete가 malloc/free보다 나은 이유

C++에서 `new`/`delete`를 쓰는 가장 큰 이유는 **생성자와 소멸자가 호출되기 때문** 이다.

```cpp
class Student {
private:
    string name;
    int score;

public:
    Student(string n, int s) : name(n), score(s) {
        cout << name << " 생성" << endl;
    }

    ~Student() {
        cout << name << " 소멸" << endl;
    }

    void print() {
        cout << name << ": " << score << "점" << endl;
    }
};

int main() {
    Student *p = new Student("홍길동", 90);  // 생성자 호출됨
    p->print();
    delete p;  // 소멸자 호출됨
    return 0;
}
```

`new`는 메모리 할당 + 생성자 호출을 한 번에 처리한다. `delete`는 소멸자 호출 + 메모리 해제를 한 번에 처리한다. `malloc`은 메모리만 잡아줄 뿐 생성자를 호출하지 않고, `free`는 메모리만 해제할 뿐 소멸자를 호출하지 않는다.

객체를 다루는 C++에서는 생성자와 소멸자가 제대로 호출되는 것이 매우 중요하다. 소멸자에서 동적 메모리를 해제하거나, 파일을 닫거나, 자원을 정리하는 코드가 있을 수 있기 때문이다. 그래서 C++에서는 `malloc`/`free` 대신 `new`/`delete`를 사용한다.

정리하면 `new`/`delete`가 `malloc`/`free`보다 나은 점은 세 가지다. 타입캐스팅이 불필요하다. `sizeof` 계산이 불필요하다. 그리고 가장 중요한 것으로, **생성자와 소멸자가 자동 호출** 된다.

### 1-4. 동적 메모리와 소멸자

클래스 내부에서 동적 메모리를 사용하면, 소멸자에서 반드시 해제해야 한다.

```cpp
class IntArray {
private:
    int *data;
    int size;

public:
    IntArray(int s) : size(s) {
        data = new int[size];  // 생성자에서 동적 할당
    }

    ~IntArray() {
        delete[] data;  // 소멸자에서 해제
        cout << "메모리 해제 완료" << endl;
    }

    int& operator[](int index) {
        return data[index];
    }
};

int main() {
    IntArray arr(5);
    arr[0] = 10;
    arr[1] = 20;
    return 0;
}  // 여기서 arr의 소멸자가 호출되어 data가 해제됨
```

생성자에서 `new`로 할당하고 소멸자에서 `delete`로 해제하는 패턴은 C++에서 매우 흔하다. 이것을 **RAII(Resource Acquisition Is Initialization)** 패턴이라고 한다. 자원의 획득을 초기화 시점에, 반환을 소멸 시점에 묶어서 관리하는 방식이다.

* * *

## 2\. 대입 연산자 오버로딩

### 2-1. 대입 연산자란?

대입 연산자(`=`)는 이미 생성된 객체에 다른 객체의 값을 복사하는 연산자다. 생성과 동시에 초기화하는 복사 생성자와는 다르다.

```cpp
Student s1("홍길동", 90);
Student s2("김철수", 85);

s2 = s1;  // 대입 연산자 — 이미 존재하는 s2에 s1을 복사
```

```cpp
Student s3 = s1;  // 이건 복사 생성자! (새 객체 생성 시 초기화)
```

`s2 = s1`은 대입 연산자, `Student s3 = s1`은 복사 생성자다. 모양이 비슷해서 헷갈리지만, 핵심 차이는 **왼쪽 객체가 이미 존재하는지 여부** 다.

### 2-2. 기본 대입 연산자의 문제

대입 연산자를 직접 정의하지 않으면 컴파일러가 **기본 대입 연산자** 를 자동 생성한다. 기본 대입 연산자는 멤버를 하나씩 그대로 복사하는 **얕은 복사(Shallow Copy)** 를 수행한다.

멤버가 `int`나 `double` 같은 기본 타입뿐이면 문제없다. 하지만 멤버에 **동적 할당된 포인터** 가 있으면 심각한 문제가 생긴다.

```cpp
class MyString {
private:
    char *str;
    int len;

public:
    MyString(const char *s) {
        len = strlen(s);
        str = new char[len + 1];
        strcpy(str, s);
    }

    ~MyString() {
        delete[] str;
    }
};
```

```cpp
int main() {
    MyString a("Hello");
    MyString b("World");
    b = a;  // 기본 대입 연산자 → 얕은 복사
    return 0;
}
```

기본 대입 연산자가 실행되면 `b.str = a.str`이 된다. 두 객체의 `str`이 **같은 메모리 주소를 가리킨다.** 이때 두 가지 문제가 발생한다.

첫째, `b`가 원래 가리키던 `"World"` 메모리가 **누구도 가리키지 않게 되어 메모리 누수** 가 발생한다.

둘째, 프로그램이 끝나면 `a`와 `b`의 소멸자가 각각 호출되면서 **같은 메모리를 두 번 해제(double free)** 한다. 이로 인해 프로그램이 크래시한다.

```plaintext
얕은 복사 후 상태:

a.str ──┐
        ├──→ [ H | e | l | l | o | \0 ]
b.str ──┘

b가 원래 가리키던 메모리:
         [ W | o | r | l | d | \0 ]  ← 누구도 안 가리킴 (메모리 누수!)
```

### 2-3. 대입 연산자 오버로딩

이 문제를 해결하려면 대입 연산자를 직접 정의해서 **깊은 복사(Deep Copy)** 를 수행해야 한다.

```cpp
class MyString {
private:
    char *str;
    int len;

public:
    MyString(const char *s) {
        len = strlen(s);
        str = new char[len + 1];
        strcpy(str, s);
    }

    ~MyString() {
        delete[] str;
    }

    // 대입 연산자 오버로딩
    MyString& operator=(const MyString &other) {
        if (this == &other) {  // 자기 자신 대입 방지
            return *this;
        }

        delete[] str;  // 기존 메모리 해제

        len = other.len;
        str = new char[len + 1];  // 새 메모리 할당
        strcpy(str, other.str);   // 내용 복사

        return *this;
    }
};
```

깊은 복사 대입 연산자의 핵심 단계를 하나씩 보면 이렇다.

**자기 자신 대입 체크** — `a = a`처럼 자기 자신을 대입하는 경우를 먼저 걸러낸다. 체크하지 않으면 기존 메모리를 먼저 해제한 뒤 이미 해제된 메모리에서 복사하려는 상황이 생긴다.

**기존 메모리 해제** — 대입을 받는 쪽(`this`)이 이미 가지고 있던 동적 메모리를 먼저 `delete`한다. 이렇게 해야 메모리 누수가 발생하지 않는다.

**새 메모리 할당 후 복사** — 원본과 같은 크기의 새 메모리를 할당하고, 내용을 복사한다. 이러면 두 객체가 각자의 독립적인 메모리를 가진다.

**자기 자신의 참조 반환** — `return *this`로 대입된 객체의 참조를 반환한다. 이렇게 해야 `a = b = c` 같은 **연쇄 대입** 이 가능해진다.

```plaintext
깊은 복사 후 상태:

a.str ──→ [ H | e | l | l | o | \0 ]
b.str ──→ [ H | e | l | l | o | \0 ]  ← 별도의 새 메모리

각 객체가 독립적인 메모리를 가지므로 double free 문제 없음
```

JavaScript에서 객체를 `=`로 대입하면 참조만 복사되는 것(얕은 복사)과 비슷한 문제다. JS에서 스프레드 연산자(`{...obj}`)나 `structuredClone`으로 깊은 복사를 하는 것처럼, C++에서는 대입 연산자를 오버로딩해서 깊은 복사를 구현하는 것이다.

### 2-4. Rule of Three

동적 메모리를 가진 클래스에서는 다음 세 가지를 반드시 함께 정의해야 한다. 이것을 **Rule of Three** 라고 한다.

**소멸자** — 동적 메모리를 해제한다. **복사 생성자** — 새 객체를 만들 때 깊은 복사를 수행한다. **대입 연산자** — 기존 객체에 대입할 때 깊은 복사를 수행한다.

이 중 하나라도 빠지면 얕은 복사로 인한 문제가 발생할 수 있다. 세 가지는 항상 세트로 생각해야 한다.

```cpp
class MyString {
public:
    MyString(const char *s);                    // 생성자
    ~MyString();                                // 소멸자
    MyString(const MyString &other);            // 복사 생성자
    MyString& operator=(const MyString &other); // 대입 연산자
};
```

셋 중 하나를 직접 정의해야 할 상황이라면, 나머지 둘도 반드시 정의해야 한다는 규칙이다. 컴파일러가 자동 생성하는 기본 버전은 전부 얕은 복사를 하므로, 동적 메모리가 있는 클래스에서는 기본 버전에 의존하면 안 된다.

* * *

## 마무리

`new`/`delete`는 C++에서 동적 메모리를 관리하는 핵심 도구다. `malloc`/`free`와 달리 생성자와 소멸자를 자동 호출해주므로, 객체의 생명주기를 제대로 관리할 수 있다. 그리고 동적 메모리를 가진 객체를 복사하거나 대입할 때는 반드시 깊은 복사를 구현해야 한다. 대입 연산자 오버로딩은 그 핵심 방법이고, 소멸자, 복사 생성자와 함께 Rule of Three로 묶어서 관리하는 것이 안전하다.
