카테고리 없음

[C++]C++ 객체 수명 주기: 소멸자와 메모리 관리 이해하기

thisisamrd 2024. 8. 8.

메모리 관리와 프로그램 성능 최적화에 있어서 매우 중요한 C++에서의 변수 저장 및 함수 인자 전달 방법, 소멸자에 대해 알아보겠습니다.

 

 

 

 

 

 

C++에서의 변수 저장 방식

 

1. 직접 저장

기본적으로 C++에서는 변수가 메모리에 직접 저장됩니다. 이 경우 변수는 그 타입에 따라 정해진 크기만큼 메모리를 차지합니다.

직접 저장의 특징은 다음과 같습니다.

 

- 수정자 없음: 타입에 특별한 기호가 없습니다.

- 크기: 변수는 자신의 타입에 맞는 크기만큼 메모리를 차지합니다.

 

Cube c;              // Cube 객체를 메모리에 직접 저장
int i;               // 정수를 메모리에 직접 저장
uiuc::HSLAPixel p;   // 픽셀을 메모리에 직접 저장

 

이 예시에서 c, i, p는 각각 Cube, int, uiuc::HSLAPixel 타입에 맞게 메모리에 직접 저장됩니다.

 

 

2. 포인터를 통한 저장(Storage by Pointer)

포인터는 다른 변수의 메모리 주소를 저장하며, 이를 통해 실제 데이터를 간접적으로 접근할 수 있습니다. 포인터를 통한 저장의 특징은 다음과 같습니다.

 

- 타입 수정자: 포인터는 타입 뒤에 별표(*)가 붙습니다.

- 메모리 주소: 포인터 자체는 메모리 주소의 크기만큼 공간을 차지합니다(예: 64비트 시스템에서는 64비트).

- 포인팅: 포인터는 실제 데이터가 저장된 메모리 위치를 가리킵니다.

 

Cube *c;          // Cube 객체에 대한 포인터
int *i;           // 정수에 대한 포인터
uiuc::HSLAPixel *p; // 픽셀에 대한 포인터

 

위 예시에서는 c, i, p가 각각 Cube 객체, 정수, 픽셀에 대한 포인터입니다. 이 포인터들은 데이터가 저장된 메모리 주소를 가리키고 있습니다.

 

 

3. 참조를 통한 저장(Storage by Reference)

참조는 기존 메모리에 대한 별칭(alias)을 만드는 방법으로, 특정 변수의 다른 이름이라고 생각하시면 됩니다. 

참조를 통한 저장의 특징은 다음과 같습니다.

 

- 타입 수정자: 참조는 타입 뒤에 앰퍼샌드(&)가 붙습니다.

- 메모리: 참조는 실제 메모리를 차지하지 않으며, 다른 변수의 별칭입니다.

- 초기화: 참조는 초기화 시에 반드시 다른 변수에 연결되어야 합니다.

 

Cube &c = cube;  // `cube` 변수에 대한 참조
int &i = count;  // `count` 변수에 대한 참조

 

여기서 c와 i는 각각 cube와 count의 별칭이 됩니다.

 

 

 

 

 

함수 인자 전달 방식

함수에 인자를 전달할 때에도 세 가지 방식(값 전달, 포인터 전달, 참조 전달)이 있습니다. 

 

1. 값 전달(Pass by Value)

기본적으로 함수에 인자를 값으로 전달하면 함수는 전달받은 인자의 복사본을 사용합니다. 아래 예시 코드를 보면서 실행 순서와 원리에 대해 함께 보겠습니다.

#include <iostream>

class Cube {
public:
    // 생성자
    Cube(double length) : length_(length) {
        std::cout << "Created Cube with volume $" << getVolume() << std::endl;
    }
    
    // 복사 생성자
    Cube(const Cube & obj) : length_(obj.length_) {
        std::cout << "Created Cube with volume $" << getVolume() << " via copy" << std::endl;
    }
    
    // 볼륨 계산 함수
    double getVolume() const {
        return length_ * length_ * length_;
    }

private:
    double length_;
};

// Cube를 값으로 전달하는 함수
bool sendCube(Cube c) {
    std::cout << "Using Cube with volume $" << c.getVolume() << std::endl;
    // Cube의 복사본을 사용
    return true;
}

int main() {
    // 길이 10인 Cube 생성
    Cube c(10);

    // Cube를 값으로 전달
    sendCube(c);

    return 0;
}

 

 

실행되는 순서 및 원리

 

  • Cube 객체 생성: int main() 함수에서 Cube c(10);이 실행되면, 길이가 10인 Cube 객체가 생성됩니다. 이 과정에서 Cube 클래스의 생성자 Cube::Cube(double length)가 호출됩니다.
  • 값 전달: sendCube(c);가 호출되면, c의 복사본이 만들어져서 sendCube 함수로 전달됩니다. 여기서 복사본을 생성하기 위해 Cube 클래스의 복사 생성자 Cube::Cube(const Cube & obj)가 호출됩니다.
  • 함수 실행: sendCube 함수는 복사본인 Cube c를 사용하여 로직을 실행합니다. 이 예시에서는 별도의 로직 없이 단순히 true를 반환합니다.
  • 함수 종료: sendCube 함수가 종료되면, sendCube 함수 내부에서 사용된 Cube c 복사본은 소멸됩니다.
  • 프로그램 종료: int main() 함수가 종료되면서 프로그램이 종료됩니다.

 

코드 실행 결과

Created Cube with volume $1000
Created Cube with volume $1000 via copy
Using Cube with volume $1000

 

 

  • 첫 번째 줄: Cube c(10);에 의해 길이 10의 Cube 객체가 생성되고, 볼륨은 1000이 됩니다.
  • 두 번째 줄: sendCube(c);가 호출되면서, c의 복사본이 생성됩니다. 복사 생성자가 호출되어 복사본의 볼륨도 1000이 됩니다.
  • 세 번째 줄: sendCube 함수 내부에서 복사된 Cube 객체의 볼륨을 사용합니다.

 

 

2. 참조 전달(Pass by Reference)

참조를 사용하여 함수에 인자를 전달하면 함수는 실제 인자를 수정할 수 있습니다.

 

#include <iostream>

class Cube {
public:
    // 생성자
    Cube(double length) : length_(length) {
        std::cout << "Created Cube with volume $" << getVolume() << std::endl;
    }
    
    // 볼륨 계산 함수
    double getVolume() const {
        return length_ * length_ * length_;
    }

private:
    double length_;
};

// Cube를 참조로 전달하는 함수
bool sendCube(Cube &c) {
    std::cout << "Using Cube with volume $" << c.getVolume() << std::endl;
    // 원본 Cube 객체를 사용
    return true;
}

int main() {
    // 길이 10인 Cube 생성
    Cube c(10);

    // Cube를 참조로 전달
    sendCube(c);

    return 0;
}

 

 

실행되는 순서 및 원리

 

  • Cube 객체 생성: int main() 함수에서 Cube c(10);이 실행되면, 길이가 10인 Cube 객체가 생성됩니다. 이 과정에서 Cube 클래스의 생성자 Cube::Cube(double length)가 호출됩니다.
  • 참조 전달: sendCube(c);가 호출되면, c의 참조가 sendCube 함수로 전달됩니다. 이때 원본 객체 c가 함수 내에서 사용됩니다.
  • 함수 실행: sendCube 함수는 참조를 통해 전달받은 원본 Cube c를 사용하여 로직을 실행합니다.
  • 함수 종료: sendCube 함수가 종료되면, sendCube 함수 내에서 사용된 참조는 더 이상 사용되지 않습니다. 하지만 원본 Cube 객체는 여전히 존재합니다.
  • 프로그램 종료: int main() 함수가 종료되면서 프로그램이 종료됩니다.

 

코드 실행 결과

Created Cube with volume $1000
Using Cube with volume $1000

 

 

 

  • 첫 번째 줄: Cube c(10);에 의해 길이 10의 Cube 객체가 생성되고, 볼륨은 1000이 됩니다.
  • 두 번째 줄: sendCube(c);가 호출되면서, c의 참조가 전달됩니다. 함수 내에서 원본 Cube 객체를 직접 사용하여 그 볼륨을 출력합니다.

 

3. 포인터 전달(Pass by Pointer)

포인터를 사용하여 인자를 전달할 수도 있습니다. 이 경우 함수는 포인터를 통해 원본 데이터에 접근합니다.

 

#include <iostream>

class Cube {
public:
    // 생성자
    Cube(double length) : length_(length) {
        std::cout << "Created Cube with volume $" << getVolume() << std::endl;
    }
    
    // 볼륨 계산 함수
    double getVolume() const {
        return length_ * length_ * length_;
    }

private:
    double length_;
};

// Cube를 포인터로 전달하는 함수
bool sendCube(Cube *c) {
    if (c == nullptr) {
        std::cerr << "Error: Null pointer received." << std::endl;
        return false;
    }
    
    std::cout << "Using Cube with volume $" << c->getVolume() << std::endl;
    // 포인터를 통해 원본 Cube 객체를 사용
    return true;
}

int main() {
    // 길이 10인 Cube 생성
    Cube c(10);

    // Cube의 포인터를 전달
    sendCube(&c);

    return 0;
}

 

 

 

실행되는 순서 및 원리

 

  • Cube 객체 생성: int main() 함수에서 Cube c(10);이 실행되면, 길이가 10인 Cube 객체가 생성됩니다. 이 과정에서 Cube 클래스의 생성자 Cube::Cube(double length)가 호출됩니다.
  • 포인터 전달: sendCube(&c);가 호출되면, c의 주소가 sendCube 함수로 전달됩니다. sendCube 함수는 이 주소를 통해 Cube 객체에 접근할 수 있습니다.
  • 함수 실행: sendCube 함수는 포인터를 통해 전달받은 Cube 객체를 사용하여 로직을 실행합니다.
  • 함수 종료: sendCube 함수가 종료되면, 함수 내에서 사용된 포인터는 더 이상 사용되지 않습니다. 하지만 원본 Cube 객체는 여전히 존재합니다.
  • 프로그램 종료: int main() 함수가 종료되면서 프로그램이 종료됩니다.

 

코드 실행 결과

Created Cube with volume $1000
Using Cube with volume $1000

 

 

 

  • 첫 번째 줄: Cube c(10);에 의해 길이 10의 Cube 객체가 생성되고, 볼륨은 1000이 됩니다.
  • 두 번째 줄: sendCube(&c);가 호출되면서, c의 메모리 주소가 전달됩니다. 함수 내에서 이 주소를 통해 원본 Cube 객체를 직접 사용하여 볼륨을 출력합니다.

 

 

 

 

C++의 소멸자란?

 

  • 클래스 소멸자는 클래스의 객체가 삭제될 때 호출되는 함수입니다. 클래스의 수명 주기에서 마지막으로 호출되는 함수입니다.
  • 자동 기본 소멸자(Automatic Default Destructor)는 클래스에 소멸자가 명시적으로 정의되지 않은 경우 자동으로 추가됩니다. 이 소멸자는 모든 멤버 객체의 기본 소멸자를 호출합니다.
  • 소멸자 호출은 직접적으로 이루어지지 않으며, 객체의 메모리가 시스템에 의해 회수될 때 자동으로 호출됩니다.
    • 스택에 있는 객체는 함수가 반환될 때 소멸자가 호출됩니다.
    • 힙에 있는 객체는 delete가 호출될 때 소멸자가 호출됩니다.

 

사용자 정의 소멸자(Custom Destructor)

사용자 정의 소멸자를 통해 객체의 수명 종료 시 추가적인 정리 작업을 수행할 수 있습니다. 사용자 정의 소멸자는 클래스 이름 앞에 물결표 ~를 붙여 정의하며, 매개변수가 없고 반환형도 없습니다.

 

Cube::~Cube() {
    cout << "Destroyed $" << getVolume() << endl;
}

 

 

 

코드 예시 및 설명

#include <iostream>

class Cube {
public:
    // 기본 생성자
    Cube() {
        std::cout << "Created $1 (default)" << std::endl;
    }

    // 매개변수가 있는 생성자
    Cube(double length) : length_(length) {
        std::cout << "Created $" << getVolume() << std::endl;
    }

    // 복사 생성자
    Cube(const Cube & obj) : length_(obj.length_) {
        std::cout << "Created $" << getVolume() << " via copy" << std::endl;
    }

    // 사용자 정의 소멸자
    ~Cube() {
        std::cout << "Destroyed $" << getVolume() << std::endl;
    }

    // 복사 할당 연산자
    Cube & operator=(const Cube & obj) {
        std::cout << "Transformed $" << getVolume() << "-> $" << obj.getVolume() << std::endl;
        length_ = obj.length_;
        return *this;
    }

    // 볼륨 계산 함수
    double getVolume() const {
        return length_ * length_ * length_;
    }

private:
    double length_;
};

// 스택에 Cube 객체 생성
double cube_on_stack() {
    Cube c(3); // c가 스택에 생성되고, 함수가 종료될 때 소멸자 호출
    return c.getVolume();
}

// 힙에 Cube 객체 생성
void cube_on_heap() {
    Cube * c1 = new Cube(10); // c1이 힙에 생성됨
    Cube * c2 = new Cube;     // c2가 힙에 생성됨
    delete c1; // c1의 소멸자가 호출됨
    // c2에 대한 소멸자가 호출되지 않음 (메모리 누수 가능성)
}

int main() {
    cube_on_stack(); // cube_on_stack() 호출
    cube_on_heap();  // cube_on_heap() 호출
    cube_on_stack(); // cube_on_stack() 다시 호출
    return 0;
}

 

코드 실행 결과 및 설명

Created $27
Destroyed $27
Created $1000
Created $1 (default)
Destroyed $1000
Created $27
Destroyed $27

 

  • cube_on_stack 함수:
    • Cube c(3);에 의해 스택에 Cube 객체가 생성됩니다.
    • 함수가 종료될 때 Cube 객체의 소멸자가 자동으로 호출되어 객체가 정리됩니다.
  • cube_on_heap 함수:
    • new Cube(10); 및 new Cube;에 의해 힙에 Cube 객체가 생성됩니다.
    • delete c1; 호출 시 c1의 소멸자가 호출되어 메모리가 해제됩니다.
    • c2는 delete되지 않아서 소멸자가 호출되지 않으며, 메모리 누수가 발생할 수 있습니다.

 

사용자 정의 소멸자의 중요성

 

  • 자원 해제: 객체가 외부 자원(예: 파일, 네트워크 연결, 동적 메모리)을 사용하는 경우, 소멸자는 이를 해제하거나 닫는 데 사용됩니다.
  • 메모리 관리: 동적 메모리 할당을 사용한 객체는 소멸자에서 delete 또는 free를 호출하여 메모리를 해제합니다.

 

댓글