devseop08 님의 블로그

[OOP] 생성자와 소멸자 본문

Language/C++

[OOP] 생성자와 소멸자

devseop08 2025. 6. 8. 16:37

생성자와 소멸자

생성자

  • 처음 객체가 만들어졌을 때에는 그 객체의 초기 상태가 적절히 지정되어 있어야 한다.
  • 객체가 만들어질 때 반드시 수행해야 하는 초기화 과정을 실수로 누락하면 프로그램은 올바르게 동작하지 않는다. => 반드시 수행해야 하는 초기화 과정을 자동화할 필요가 있다.
  • 객체의 초기화 과정을 자동화하여 수행해줄 목적으로 사용하는 것이 바로 생성자이다
  • 생성자의 특징은 다음과 같다.
    • 특수한 멤버 함수
    • 객체가 생성될 때 자동으로 호출(객체 정의 시 자동 호출)
    • 초기화 목적으로 사용
    • 클래스와 동일한 이름을 갖는 멤버 함수
    • 반환 타입이 없다.
    • 오버로딩이 가능하다.
  • 생성자 선언(구현)
class ClassName {

public:    // public으로 선언돼야지만 외부에서 해당 타입의 객체를 생성할 수 있다.
    ClassName(parameterList) { ... } // 생성자
}
// Counter.hpp

#ifndef COUNT_H_INCLUDED
#define COUNT_H_INCLUDED

class Counter {

    int value; // 접근 제한 private(디폴트) 

public:
    Counter(){ value = 0; }    // 생성자

    void reset(){ value = 0; }

    void count(){ ++value; }

    int getValue() const { return value; }
}

#endif
  • 객체 정의와 생성자 자동 호출
// CntMain.cpp
include <iostream>
include "Counter.hpp"

int main(){

    Counter counter; // 객체 정의, Counter 객체 생성자 자동 호출 => value 자동으로 0 

    cout << "계수기의 현재 값: " << cnt.getValue() << endl;

    return 0;
}

소멸자

  • 객체가 소멸될 때 자동으로 실행되는 함수
  • 객체의 소멸에 따라 필요한 제반 처리를 하기 위한 코드가 포함
  • 소멸자의 특성은 다음과 같다
    • 특수한 멤버 함수
    • 객체가 소멸할 때 자동으로 호출
    • 메모리 및 기타 리소스 해제 목적으로 유용하게 사용
    • 클래스와 동일 이름 앞에 '~'을 갖는 멤버 함수
    • 반환 타입 및 파라미터는 존재하지 않는다.
    • 오버로딩 불가
    • 객체 또는 객체의 포인터가 소멸되는 시점에 자동으로 호출
  • 소멸자 선언(구현)
class ClassName {

public:
    ~ClassName() { ... } // 소멸자!!
}
// Person.hpp

#ifndef PERSON_H_INCLUEDED
#define PERSON_H_INCLUEDED

class Person {
    char* name;
    char* addr;
public:
    Person(const char* name, const char* addr); // 생성자
    ~Person(); // 소멸자!!
    void print() const; // 이름과 주소 출력
    void chAddr(const char* newAddr);
}

#endif
// Person.cpp
include <iostream>
include <cstring>
include "Person.hpp"

using namespace std;

Person::Person(const char* name, const char* addr) // 생성자: 객체 생성 시 호출 실행
{
    this -> name = new char[strlen(name)-1]; 
    // this는 현재 객체 자신의 포인터 변수
    // 문자배열 공간 확보, 동적 메모리 할당!!

    strcpy(this->name, name);

    this -> addr = new char[strlen(addr)-1]; 
    // this는 현재 객체 자신의 포인터 변수
    // 문자배열 공간 확보, 동적 메모리 할당!!

    strcpy(this->addr, addr);
}

Person::~Person() //  소멸자: 객체 소멸 시 자동 호출 실행, "객체 소멸" 시 호출!!
{
    cout << "Person 객체 제거함(" << name << ")"" << endl;
    delete[] name;  // 동적 할당된 메모리 공간(배열 공간) 해제
    delete[] addr;  // 동적 할당된 메모리 공간(배열 공간) 해제
}

void Person::print() const
{
    cout << addr << "에 사는 " << name << "입니다" << endl;
}

void Person::chAddr(const char* newAddr)
{
    delete[] addr;  // 동적 할당된 메모리 공간(배열 공간) 해제

    addr = new char[strlen(newAddr)-1];  
    // this 생략 가능, this -> addr과 동일
    // 동적 메모리 할당

    strcpy(addr, newAddr); // this 생략 가능, this -> addr과 동일
}
// PrsnMain.cpp
include <iostream>
include "Person.hpp"

using namespace std;

int main(){
    Person p1{"박상훈", "경기도 수원시"}; 
    // 스택에 Person 타입 객체 할당, scope 닫힐 때 소멸자 자동 호출
    Person* p2 = new Person("이철수", "서울시 종로구");
    // 힙 영역에 Person 타입 객체 동적 할당 
    // scope 닫혀도 소멸되지 않음 => delete 연산 필요
    Person* p3 = new Person("박은미", "강원도 동해시");
    // 힙 영역에 Person 타입 객체 동적 할당 
    // scope 닫혀도 소멸되지 않음 => delete 연산 필요

    p1.print();
    p2->print();
    p3->print();

    cout << endl << "주소 변경: ";
    p1.chAddr("경기도 성남시");
    p1.print();
    p2->chAddr("대전시 서구");
    p2->print();

    delete p2;  // p2가 가리키는 Person 객체 소멸
    delete p3;  // p3가 가리키는 Person 객체 소멸
}  // p1에 저장된 객체 소멸

생성자 오버로딩

  • 생성자도 멤버 함수와 마찬가지로 오버로딩 가능
  • 멤버 함수와 동일한 오버로딩 규칙이 적용된다. : 각각의 생성자는 고유해야 한다, 즉 매개변수가 달라야 한다.
// Player.hpp
class Player {
private:
    std::string name;
    int helth;
    int xp;
public: 
    Player();
    Player(std::String nameVal);
    Player(std::string nameVal, int healthVal, int xpVal);
}
// Player.cpp
Player::Player()
{
    name = "NONE";
    health = 0;
    xp = 0;
}

Player::Player(std::String nameVal)
{
    name = nameVal;
    health = 0;
    xp = 0;
}

Player::Player(std::string nameVal, int healthVal, int xpVal)
{
    name = nameVal;
    health = healthVal;
    xp = xpVal;
}
// Main.cpp
int main(){
    Player empty; // None, 0, 0
    Player hero {"Hero"}; // Hero, 0, 0 
    Player kim {"Kim", 100, 5}; // Kim, 100, 5
    Player* enemy = new Player{"Enemy", 1000, 0}; // Enemy, 1000, 0
    delete enemy; 
}
  • 생성자 초기화 리스트
    • 생성자 초기화 리스트를 사용할 경우 생성과 동시에 즉시 값이 지정됨
    // Player.cpp
    
    Player::Player()
           : name{"None"}, health{0}, xp{0}
    {
    
    }
    
    Player::Player(std::string nameVal)
           : name{nameVal}, health{0}, xp{0}
    {
    
    }
    
    Player::Player(std::string nameVal, int healthVal, int xpVal)
           : name{nameVal}, health{healthVal}, xp {xpVal}
    {
    
    }
    • 생성자 위임
      • 생성자 오버로딩으로 인해 유사한 코드 중복되는 생성자들이 발생
        -> 오류 발생 가능성이 높아짐
        -> 생성자 위임을 통해서 코드 중복을 제거하고 오류 가능성을 낮출 수 있음
      • 다른 생성자를 초기화 리스트 자리에서 호출
      • 생성자 초기화 리스트를 사용해서만 가능
      // Player.cpp
      Player::Player()
            : Player{"None", 0, 0} // 생성자 위임 호출
      {
      }
      
      Player::Player(std::string nameVal)
           : Player{nameVal, 100, 50} // 생성자 위임 호출
      {
      }
      
      Player::Player(std::string nameVal, int health, int xp)
           : name{nameVal}, health{health}, xp{xp}
      {
      }
    • 생성자 기본 매개변수
      • 생성자 또한 함수이므로, 기본 매개변수 사용 가능
      // Player.hpp
      class Player {
      private:
        std::string name;
        int helth;
        int xp;
      public: 
        Player(std::string nameVal="None", int healthVal=0, int xpVal=0);
      }
      // Player.cpp
      Player::Player(std::string nameVal, int helthVal, int xpVal)
           : name {nameVal}, health {healthVal}, xpVal {xpVal}
      {
      
      }
      // Main.cpp
      Player empty; // None, 0, 0
      Player hero {"Hero"}; // Hero, 0, 0 
      Player kim {"Kim", 100, 5}; // Kim, 100, 5
      Player* enemy = new Player{"Enemy", 1000, 0}; // Enemy, 1000, 0
      delete enemy; 

this

  • this는 C++ 키워드
  • 멤버 함수를 호출한 실제 객체의 주소값을 저장한 포인터 타입의 공간
  • 사용 예
void Player::setPosition(int x, int y)
{
    this -> x = x;
    this -> y = y;
}

생성자의 종류

디폴트 생성자(기본 생성자)

  • 인자가 없는 생성자
  • 클래스의 생성자를 직접 구현하지 않으면, 컴파일러가 기본적으로 만들어줌
  • 객체를 인자없이 정의하면 됨.
  • 인자가 있는 생성자만 구현한 경우 기본 생성자가 자동 생성되지 않는다.

복사 생성자

  • 객체가 복사될 때, 기존 객체를 기반으로 새로운 객체가 생성됨
  • 객체가 복사되는 경우
      1. 객체를 pass by value 방식으로 함수의 매개변수로 전달할 때
      1. 함수에서 value의 형태로 결과를 반환할 때
      1. 기존 객체를 기반으로 새로운 객체를 생성할 때
      Player hero {1, 1, 1};
      
      Player another_hero = hero // 복사 생성자 호출, Player another_hero{hero}와 동일
      
      Player another_hero;
      
      another_hero = hero; // 복사 생성자 호출되지 않는다!! 단순 대입 연산이다
  • 객체가 어떤 방식으로 복사될 지를 결정해줘야 한다. 즉, 복사 생성자를 선언(구현)해줘야 한다.
    • 사용자가 구현하지 않는 경우 컴파일러에서 자동으로 복사 생성자를 구현
  • 복사 생성자가 중요한 이유
    • 복사하는 객체의 멤버로 포인터가 있는 경우, 복사 과정에서 문제가 발생할 수 있기 때문에 이를 잘 처리해줘 한다.
    • 복사 비용에 대한 인식
  • 자동 생성되는 복사 생성자
    • 멤버 변수들의 값을 복사하여 대입하는 방식
    • 포인터 타입의 멤버 변수가 존재할 때는 주의
      • 기본 복사 생성자는 포인터 타입의 변수 또한 복사하여 대입됨
      • 즉, 포인터가 가리키는 데이터의 복사가 아닌 포인터 주소값의 복사
    • 얕은 복사 vs 깊은 복사
  • 복사 생성자 선언(복사하려는 객체 타입의 const 참조자를 인수로 사용한다.)
Type::Type(const Type& source){...}
Player::Player(const Player& source){...}
Account::Account(const Account& source){...}
얕은 복사와 깊은 복사
  • 얕은 복사의 문제점 : 아직 해제되면 안 되는 동적 할당된 메모리 공간을 해제할 수 있다.
// Shallow.hpp
class Shallow 
{
private: 
    int *data;
public: 
    Shallow(int d);
    Shallow(const Shallow& source);
    ~Shallow();
}
// Shallow.cpp

Shallow::Shallow(int d)  
// 생성자 구현: int 동적 메모리 할당하고 포인터 타입 멤버가 가리키는 공간에 값 저장
{
    data = new int;
    *data = d;
}

Shallow::Shallow(const Shallow& source) 
        : data {source.data}       
        // 얕은 복사를 수행하는 복사 생성자(주소를 복사)
{
    cout << "Copy constructor, shallow" << endl;
}

Shallow::~Shallow()      // 소멸자 구현: 
{
    delete data;         
    // 객체 안에 포인터 타입 변수(data)가 가리키는 동적 할당된 메모리 공간 해제
    cout << "free storage" << endl;
}
// Main.cpp

void displayShallow(Shallow shallow){ // 복사 생성자 호출
     cout << "value : " << *(shallow.data);
} // 소멸자 호출

int main(){
    Shallow obj{100};
    displayShallow(obj) // value : 100 출력
    return 0;
} // 소멸자 호출 -> ERROR: 이미 해제된 obj.data가 가리키는 공간을 소멸자로 또 해제 시도 
  • displayShallow 함수 호출 시 Shallow 복사 생성자 호출 -> displayShallow 함수가 종료되면서 Shallow 소멸자 호출
  • displayShallow 함수 호출하면서 복사 생성된 객체 안의 data가 가리키는 공간과 기존의 obj 객체 안의 data가 가리키는 공간이 동일하므로
  • displayShallow 함수 종료되면서 소멸자 호출로 인해 해제되는 공간은 기존의 obj 객체 안의 data가 가리키는 공간과 동일하다. => 해제되선 안되는 데 의도치 않게 해제되었다.
  • 깊은 복사로 해결
// Deep.hpp
class Deep 
{
private: 
    int *data;
public: 
    Deep(int d);
    Deep(const Deep& source);
    ~Deep();
}
// Deep.cpp

Deep::Deep(int d)  
// 생성자 구현: int 동적 메모리 할당하고 포인터 타입 멤버가 가리키는 공간에 값 저장
{
    data = new int;
    *data = d;
}

Deep::Deep(const Deep& source)   
{
    data = new int;
    *data = *(source.data);
    cout << "Copy constructor, deep" << endl;
}

Deep::~Deep()      // 소멸자 구현: 
{
    delete data;         
    // 객체 안에 포인터 타입 변수(data)가 가리키는 동적 할당된 메모리 공간 해제
    cout << "free storage" << endl;
}
// Main.cpp

void displayDeep(Deep deep){ // 복사 생성자 호출
     cout << "value : " << *(deep.data);
} // 소멸자 호출

int main(){
    Deep obj{100};
    displayDeep(obj) // value : 100 출력
    return 0;
} 

이동 생성자

// VecF.h
#ifndef VEC_F_H_INCLUDED
#define VEC_F_H_INCLUDED
#include <iostream>
#include <cstring>
using namespace std;

class VecF{
    int n;
    float* arr;
public: 
    VecF(int d, float* a=nullptr): n {d} { // 생성자
        arr = new floatp[d];
        if(a){
            memcpy(arr, a, sizeof(float) * n);
        }
    }

    VecF(const VecF& fv): n {fv.n} { // 복사 생성자
        arr = new float[n];
        memcpy(arr, fv.arr, sizeof(float) * n);
    }

    ~VecF(){
        deletep[] arr;
    }

    VecF add(const VecF& fv) const {
        VecF tmp(n);
        for(int i = 0;  i < n; i++){
            tmp.arr[i] = arr[i] + fv.arr[i];
        }
        return tmp;
    }

    void print() const {
        cout << "[ ";
        for(int i = 0; i < n; i++){
            cout << arr[i] << " ";
        }
        cout << "]";
    }
}
#include <iostream>
using namespace std;
#include "VecF.h"

int main(){
    float arr[3] = {1, 2, 3};
    VecF v1(3, arr);
    VecF v2(v1); 
    VecF v3(v1.add(v2)); 
}
  • VecF v2(v1);VecF v3(v1.add(v2)); 모두 기존에 만들어진 객체를 기반으로 객체를 생성하기 때문에 두 구문 모두에서 복사 생성자가 호출되어 깊은 복사가 이루어진다.
  • 하지만 VecF v2(v1);에서 v1은 복사 생성자 호출 스택이 종료돼도 객체가 소멸되지 않지만 VecF v3(v1.add(v2));에서 인자로 주어지는 객체는 복사 생성자 호출 스택이 종료되면 객체가 소멸되는 임시 객체이다.
  • 애초에 복사 호출 스택이 종료되면 바로 소멸될 임시 객체를 깊은 복사까지 하면서 새로운 객체를 생성하는 것은 비효율적이다.
  • 호출 스택이 종료되면 바로 소멸될 임시 객체를 깊은 복사하지 않으면서 새로운 객체를 생성하기 위한 목적의 생성자를 따로 만들어 이 비효율성을 해결할 수 있다.
  • v1.add(v2)의 반환값은 r-value인 점을 이용해 r-value 참조를 파라미터로 하는 생성자를 만들어주면 된다.
  • r-value 참조를 인자로 받는 생성자에서는 인자로 받은 임시 객체 안의 필드값들을 새로 생성하려는 객체의 필드로 옮겨준 후 임시 객체의 필드들을 전부 초기화 시켜주면 된다.
  • 이러한 생성자를 이동 생성자라 한다.
class VecF{
    ...
public: 
    VecF(int d, float* a=nullptr): n {d} { // 생성자
        arr = new floatp[d];
        if(a){
            memcpy(arr, a, sizeof(float) * n);
        }
    }

    VecF(const VecF& fv): n {fv.n} { // 복사 생성자
        arr = new float[n];
        memcpy(arr, fv.arr, sizeof(float) * n);
    }

    VecF(VecF&& fv): n {fv.n}, arr {fv.arr} { // 이동 생성자
        // fv가 r-value 참조하는 객체는 이동 생성자를 호출한 스택 종료 이후로는 소멸된다.
        // 즉, 복사 객체를 위한 데이터 공간을 새로 동적 할당할 필요가 없다는 것이다.
        fv.arr = nullptr;
        fv.n = 0;
    }

    ~VecF(){
        deletep[] arr;
    }

    ...
}

동적 할당한 객체 복사 생성

Buffer* p = new Buffer(5);

// 1) 객체 복사 (깊은 복사): 복사 생성자 호출
Buffer b1 = *p;

// 2) 포인터 복사 (얕은 복사): 주소만 복사
Buffer* p2 = p;

// 정리는 반드시 한 번만:
delete p;      // p와 p2는 같은 객체를 가리켰으므로, 한 번만 delete해야 함
// delete p2;  // 다시 지우면 이중 해제(UB)

static 데이터 멤버와 static 멤버 함수

static 키워드

  • 멤버 함수들은 개별 객체에 대한 상태를 저장하는 것이 아니므로 멤버 함수들을 위한 프로그램 메모리 공간은 개별 객체가 아닌 클래스마다 하나씩만 있으면 된다.
  • 하지만 데이터 멤버는 각각의 객체별 상태를 저장해야 하므로 객체가 정의되면 그 객체의 데이터 멤버를 위해 고유한 메모리 공간이 할당된다.
  • 그러나 때로는 데이터 멤버를 클래스에 속한 객체들이 함께 공유하는 것이 필요할 때가 있다.
  • 변수를 공유하기 위해 전역변수를 사용할 수도 있지만 전역변수는 특정 클래스의 객체들만 공유하는 것이 아니어서 프로그램 해석을 어렵게 한다.
  • 따라서 특정 클래스에 속하는 객체들끼리만 공유할 수 있는 공간을 static이라는 키워드로 할당할 수 있게 하였다.

  • static 클래스 멤버 변수
    • 객체가 아닌 클래스에 속하는 변수
    • 개별적인 객체의 데이터가 아닌 클래스에 공통 데이터 구현이 필요할 때 사용
  • static 클래스 멤버 함수
    • 멤버 함수도 static으로 정의할 수 있는데 static 멤버 함수는 특정 객체에 대한 처리를 하는 것이 아니라 클래스의 이름으로 어떠한 작업을 수행하는 함수이다.
    • 객체가 아닌 클래스에 속하는 함수
    • 클래스 이름 하에서 바로 호출 가능
    • static 클래스 멤버 함수는 static 클래스 멤버 변수에만 접근 가능

friend 함수와 friend 클래스

  • friend 함수와 friend 클래스는 클래스의 멤버가 아니다.
  • friend 키워드
  • 특정 클래스의 private 멤버에 접근할 수 있는 함수나 클래스를 선언(구현)할 때 사용
  • 비대칭 : A가 B 클래스의 friend라 해도 B 클래스가 A의 friend는 아니다.
  • 전이되지 않음: A가 B 클래스의 friend이고 B 클래스가 C 클래스의 friend라고 해서 A가 C 클래스의 friend는 아니다.
  • friend 함수
class Player {
    friend void displayPlayer(const Player& p); // Player의 friend이지 멤버가 아님
private:
    int x, y;
    int speed;
public:
    Player(int x, int y, int speed): x{x}, y{y}, speed{speed}
    {
        count << this << endl;
    }

    void setPosition(int x, int y){
        this -> x = x;
        this -> y = y;
    }
}

void displayPlayer(const Player& p){
    cout << p.x << ", " << p.y << endl; // friend가 아니었으면 접근 불가
}
  • friend 클래스
class Player {
    friend class Game; // Player의 friend이지 멤버가 아님
private:
    int x, y;
    int speed;
public:
    Player(int x, int y, int speed): x{x}, y{y}, speed{speed}
    {
        count << this << endl;
    }

    void setPosition(int x, int y){
        this -> x = x;
        this -> y = y;
    }
}

class Game {

public: 
    void getPlayer(const Player& p)
    {
        cout << p.x << ", " << p.y << endl; // friend가 아니었으면 접근 불가
    }        
}

'Language > C++' 카테고리의 다른 글

[OOP] 상속과 다형성  (0) 2025.07.15
[Basic] 상수와 리터럴  (0) 2025.07.07
[Basic] 타입과 변수  (0) 2025.06.07
[OOP] 클래스 선언과 객체 정의  (0) 2025.06.06
[Basic] 배열, 포인터, 참조  (0) 2025.06.05