티스토리 뷰

생성자와 소멸자

객체의 시작과 끝

 

이번 챕터에서는 생성자와 소멸자에 대해 알아보려 한다.

 

들어가기 전 간단하게 생성자와 소멸자를 이해한다면, 생성자는 객체를 초기화하는 함수이며 소멸자는 객체의 메모리 해제를 하는 함수이다.

 

 


생성자

시작이 반, 초기화는 생활화

 

 

생성자는 클래스의 정적 바인딩 시, 선언과 동시에 실행되는 함수로, 오버라이딩으로 매개변수를 통해 외부의 값을 받아 클래스 내부에서 사용할 수 도 있고, 기본 생성자라는 암시적 멤버 함수를 통해 객체를 초기화할 수 있다.

 

모든 객체의 생성과 소멸은 해당 객체가 선언된 스코프 내에서 발생한다.

하여 전역 변수프로그램이 시작되면 생성자를 호출하고 프로그램이 끝나면 소멸자를 호출 한다.

 

생성자는 반드시 클래스 명칭을 따라야하며, 반환 값을 가지지 않는다.

 

생성자 예제
#include <iostream>
using namespace std; 

class MyClass {
public:
    // 기본 생성자 (Default Constructor)
    MyClass() {
        cout << "Default Constructor called" << endl; // 생성자가 호출될 때 수행할 작업을 여기에 추가
    }
    
    // 매개변수 생성자 (Default Constructor)
    MyClass(int i) {
        k = i;
        cout << "Default Constructor called " << i << endl; // 생성자가 호출될 때 수행할 작업을 여기에 추가
    }

    // 소멸자 (Destructor)
    ~MyClass() {
        cout << "Destructor called " << k << endl; // 객체가 소멸될 때 수행할 작업을 여기에 추가
    }
private:
	int k = 0;
};

MyClass m1(2);

void MakeObject(){
	MyClass mc(1);	// 정적 바인딩: 객체 선언과 동시에 생성자 호출
} 			// 스코프 종료와 함께 객체의 소멸자 호출

int main() {
    MakeObject();
    return 0;
}
Default Constructor called 2	# 전역 객체 생성자
Default Constructor called 1	# 지역 객체 생성자
Destructor called 1		# 지역 객체 소멸자
Destructor called 2		# 전역 객체 소멸자
  지역 객체 전역 객체
생성 선언 시 프로그램 시작 시 (main 시작 전)
소멸 함수 종료 시 프로그램 종료 시 (main 종료 후)

 

 

생성자 활용하기

생성자는 초기화에 주로 많이 사용된다. 즉, 초기화가 아니면 접근이나 선언, 할당이 불가능한 변수를 만들 수 있다는 소리이다.

 

생성자를 통한 초기화
#include <iostream>
#include <string>

using namespace std;

class Person {
private:
    string name;
    int age;

public:
    Person() {
        this->name = "Dari";
        age = 24;
    }
    Person(string name) {
        this->name = name;
        age = 20;
    }

    // Getter for name
    string getName() const {
        return name;
    }

    // Getter for age
    int getAge() const {
        return age;
    }
};

int main() {
    Person dari;
    Person nari = Person("nari");

    // Getter 호출
    cout << "이름: " << dari.getName() << ", 나이: " << dari.getAge() << "세" << endl;
    // Getter 호출
    cout << "이름: " << nari.getName() << ", 나이: " << nari.getAge() << "세" << endl;

    return 0;
}
이름: Dari, 나이: 24세
이름: nari, 나이: 20세

 

위 코드를 보면 첫 생성자를 생성할 때, 이름과 나이를 입력할 수 있고 이후 어떠한 방법으로도(메모리 주소를 직접 건들지 않는 이상) 변경되지 않는다.

 

이렇게 한번 지정되고 변경되지 않아야하는 값이 존재하는 객체라면 위와 같은 예제로 사용할 수 있다.

 

 

기본 생성자

기본 생성자 = 디폴트 생성자라 불리는 함수는 객체 생성시 어떠한 매개변수도 받지 않으며, 실제로 클래스나 구조체에 선언하지 않아도 암시적으로 내부에 적용되어 있다.

 

기본 생성자 선언
class Person {
private:
    string name;
    int age;

public:
    Person() {	// 기본 생성자 선언
        this->name = "Dari";
        age = 24;
    }
    
    // Getter for name
    string getName() const {
        return name;
    }

    // Getter for age
    int getAge() const {
        return age;
    }
};

 

위와 같은 클래스가 작성되었을 때, 우리는 기본 생성자가 name을 "Dari"로, 나이를 24로 설정하는 것을 볼 수 있다.

즉, 우리가 객체를 생성하면 필수적으로 이름은 "Dari"이며 나이는 24살로 설정된 객체가 되는 것이다.

 

그런데 위에서 기본 생성자암시적으로 설정된다고 했다. 이게 무슨 말일까?

 

암시적 기본 생성자
class Person {
private:
    string name;
    int age;

public:
    Person() {	// 기본 생성자 선언
        this->name = "Dari";
        age = 24;
    }
    
    // Getter for name
    string getName() const {
        return name;
    }

    // Getter for age
    int getAge() const {
        return age;
    }
};

class Dog {
private:
    string name;
    int age;

public:
    // Getter for name
    string getName() const {
        return name;
    }

    // Getter for age
    int getAge() const {
        return age;
    }
};

 

위 코드를 보면 Person 클래스에는 기본생성자가 선언되어 있고, Dog 클래스에는 어떤 생성자도 없다. 그렇다면 Dog 클래스는 오류를 출력하는가?

 

기본 생성자 결과
int main() {
    Person dari;
    Dog nabi;

    // Getter 호출
    cout << "이름: " << dari.getName() << ", 나이: " << dari.getAge() << "세" << endl;
    // Getter 호출
    cout << "이름: " << nabi.getName() << ", 나이: " << nabi.getAge() << "세" << endl;

    return 0;
}
이름: Dari, 나이: 24세
이름: , 나이: 1세

 

결과를 확인하면 알겠지만, 어떠한 에러도 발생하지 않는다.

 

다만 Dog 클래스 객체 내부 값은 어떠한 값도 선언되지 않았기에 객체 내부에 초기화가 되어야 했을 변수가 원형으로 존재한다. (컴파일러에 따라 초기화 되지 않은 값이 있을 수 도 있다.)

 

이러한 상황이 발생하는 이유가 바로 클래스가 선언되면 기본적으로 우리 눈에 보이지 않는 총 6가지의 암시적 생성자와 소멸자가 존재하기 때문이다. (뒤에서 마저 이야기하겠다)

 

생성자 오버로딩

우리는 앞서 기본 생성자가 무엇인지 배웠다. 그리고 기본 생성자는 선언할 수 도, 하지 않을 수 도 있다는 것을 배웠다.

왜 그럴까? 이는 바로 생성자 오버로딩 때문이다.

 

클래스 글을 보면 오버로딩이라는 개념을 배웠을 수 있다. 오버로딩은 간단하게 원래는 동일한 이름을 가진 함수가 선언될 수 없으나, 동일한 작업을 다양한 상황에서 하기 위하여 동일한 명칭의 함수를 매개변수의 개수와 자료형의 차이로 다른 함수라고 구별하는 것이다. (반환 값으로는 구별하지 않는다.)

 

이를 통해 우리는 앞서 클래스 내부에 암시적으로 선언된 기본 생성자를 우리가 새로운 기본 생성자를 통해 오버로딩 하였고, 그 결과로 Person과 Dog 객체의 결과를 확인할 수 있었다.

 

생성자 오버로딩에는 주의할 점이 추가로 있다.

기본 생성자를 따로 선언하지 않고 다른 생성자를 하나라도 선언한다면, 암시적 기본 생성자는 사용할 수 없다는 것이다.

 

암시적 기본 생성제 제거
class Person {
private:
    string name;
    int age;

public:
    Person(string name) {	// 이름 값을 받는 생성자 선언
        this->name = name;
        age = 24;
    }
    
    // Getter for name
    string getName() const {
        return name;
    }

    // Getter for age
    int getAge() const {
        return age;
    }
};

int main() {
    //Person dari; // 에러: 기본 생성자를 호출하려 함
    Person dori = Person("dori"); // 문제 없이 진행

    // Getter 호출
    cout << "이름: " << dori.getName() << ", 나이: " << dori.getAge() << "세" << endl;

    return 0;
}
이름: dori, 나이: 24세

 

위 코드를 보면 이름 값을 받는 생성자 하나만 선언되어 있다. 그리고 main 함수를 보면 기본 생성자를 선언하려던 객체는 오류를 출력하고 생성자 포맷에 맞는 string 값을 전달하는 객체는 이상 없이 객체를 생성했다.

 

이를 통해 우리는 암시적 기본 생성자는 다른 생성자에 의해 제거된다는 사실을 확인할 수 있다.

 

 

다양한 초기화 방법들

생성자를 호출하는 방법은 다양하다. 리스트 초기화 방법도 개중 하나이다.

 

다양한 초기화 문법
class Person {
private:
    string name;
    int age;
public:
	Person() {
        this->name = "Nabi";
        this->age = 10;
    }
	Person(string name) {
        this->name = name;
        this->age = 10;
    }
    Person(string name, int age) {
        this->name = name;
        this->age = age;
    }
};


// 기본 생성자
Person human;
Person human = Person();

// 생성자 오버로딩
Person human("name");
Person human("name", 22);
Person human = Person("name");
Person human = Person("name", 22);

// 생성자 - 리스트 초기화
Person human{"name"};
Person human{"name", 22}; // 이름과 나이 초기화

 

 

생성자 활용하기

생성자는 오버로딩과 동시에 디폴트 매개변수도 선언할 수 있다.

 

디폴트 매개변수 선언
#include <iostream>
#include <string>

using namespace std;

class Person {
private:
    string name;
    int age;

public:
    Person(string name = "Dabi", int age = 10) {
        this->name = name;
        this->age = age;
    }

    // Getter for name
    string getName() const {
        return name;
    }

    // Getter for age
    int getAge() const {
        return age;
    }
};

int main() {
    Person dari; // 디폴트 생성자 호출
    Person dori("Dori"); // 매개변수가 있는 생성자 호출
    Person tada("Tada", 22); // 매개변수가 있는 생성자 호출

    cout << "이름: " << dari.getName() << ", 나이: " << dari.getAge() << "세" << endl;
    cout << "이름: " << dori.getName() << ", 나이: " << dori.getAge() << "세" << endl;
    cout << "이름: " << tada.getName() << ", 나이: " << tada.getAge() << "세" << endl;

    return 0;
}
이름: Dabi, 나이: 10세
이름: Dori, 나이: 10세
이름: Tada, 나이: 22세

 

디폴트 매개변수를 사용하는 생성자를 사용할 때는 주의 점이 있다.

바로 모든 매개변수가 디폴트 값이 정해진다면 이는 기본 생성자를 대체 한다는 것이다. 즉, 모든 매개변수의 디폴트 값이 존재하면 기본 생성자를 선언할 수 없다는 것이다.

 

 


멤버 초기화 목록 (Member Initializer List)

멤버 초기화 목록은 생성자 함수 명칭 선언 줄에 콜론(:) 다음 쉼표로 멤버들의 값을 초기화하는 것이다.

 

멤버 초기화 목록
class MyClass {
private:
    int myInt;

public:
    MyClass(int val) : myInt(val) {
        // 생성자 본체, myInt는 이미 초기화되어 있음
    }
};

 

 

멤버 초기화 목록은 다른 생성자로 매개변수를 할당하는 방법들과 다르게 초기화를 한다. 이를 이해하기 위해서는 초기화와 할당의 차이를 이해하여야 한다.

 

 

초기화 vs 할당

초기화 (Initialization) 할당 (Assignment)
초기화는 변수가 생성됨과 동시에 값을 할당한다.
변수가 사용할 메모리 공간에 실제 값을 설정한다.
이미 존재하는 변수에 값을 대입한다.
이미 생성된 변수의 값을 변경한다.
int x = 5; // 초기화
double pi = 3.14; // 초기화
string name = "John"; // 초기화

int y; // 변수 선언
y = 10; // 할당

 

위 정보가 이번 문단에서 중요한 이유는 바로 멤버 초기화 리스트가 초기화를 통해 상수 값도 초기화할 수 있기 때문이다.

 

 

상수 초기화

멤버 초기화 리스트는 선언과 동시에 값을 초기화한다. 이는 상수 값이 사용되기 위해 필요한 조건(선언과 동시에 할당되어야한다.)을 충족 시킬 수 있다.

 

상수 초기화
#include <iostream>

using namespace std;

class MyClass {
private:
    const int myConstant;

public:
    // 멤버 초기화 리스트 사용
    MyClass(int constantValue) : myConstant(constantValue) {
        // 생성자의 본체
    }

    // Getter for myConstant
    int getConstant() const {
        return myConstant;
    }
};

int main() {
    // 객체 생성 시 상수 값으로 초기화
    MyClass myObject(42);

    // Getter 호출
    cout << "상수 값 : " << myObject.getConstant() << endl;

    return 0;
}
상수 값 : 42

 

 

멤버 초기화 리스트 활용하기

멤버 초기화 리스트는 독특한 방향으로 사용이 가능하다.

 

멤버 초기화 리스트 활용
class MyClass {
public:
    Time() : h(0), m(0), s(0) {}
    Time(int s) : Time() {
    	this->s = s;
    }
    Time(int m, int s) : Time(s) {
    	this->m = m;
    }
    Time(int h, int m, int s) : Time(m, s) {
    	this->h = h;
    }
private:
	int h, m, s;
}

 

 

위 코드를 통해 우리는 재미있는 사실을 알 수 있다. 바로 멤버 초기화 리스트를 활용하여 다른 생성자를 호출 및 작업을 "위임"시키는 것이다. 이를 통해 모두 동일한 작업을 하는 생성자를 좀더 간략하게 작성할 수 있다.

 

이 방식은 동작 순서를 이해하면 알기 쉬운데, 각 생성자는 스코프가 시작되기 전 콜롬(:) 옆에 있는 생성자나 멤버 초기화 리스트를 먼저 호출한다. 따라서 위 예제의 순서도는 다음과 같다.

 

  1. 기본 생성자가 실행되어 h, m, s 변수 모두 0으로 초기화
  2. 매개변수 1개의 생성자가 실행되며 s 할당
  3. 매개변수 2개의 생성자가 실행되며 m 할당
  4. 매개변수 3개의 생성자가 실행되며 h 할당

 


클래스 암시적 함수 6가지

클래스가 선언되면 아무 작업을 하지 않아도 기본적으로 6가지의 멤버 함수가 선언된다.

 

명칭 설명
기본 생성자  클래스 내 변수 초기화를 주 목적으로 하는 생성자.
복사 생성자  같은 클래스 타입의 객체를 복사하기 위해 있는 생성자. (TestClass test2(test))
복사 대입 생성자  선언 이후 같은 클래스 간의 대입할때 쓰이는 연산자입니다. (TestClass test2 = test)
이동 생성자  복사생성자와 같이 Lvalue copy가 일어나는것이 아닌 Rvalue로 캐스팅하여 이동시켜주는 것
(Lvalue : 연산 이후 지속되는 객체, Rvalue : 연산 이후 사라지는 객체)
이동 대입 연산자  이동생성자와 같지만 대입 연산자를 오버로딩 하는것
기본 소멸자  클래스가 delete되거나 선언된 지역에서 벗어나면 호출되는 함수
#include <iostream>
#include <cstring>

using namespace std;

class MyString {
private:
    char* data;

public:
    // 기본 생성자
    MyString() : data(nullptr) {
        cout << "기본 생성자 호출" << endl;
    }

    // 복사 생성자
    MyString(const MyString& other) {
        cout << "복사 생성자 호출" << endl;
        if (other.data != nullptr) {
            data = new char[strlen(other.data) + 1];
            strcpy(data, other.data);
        } else {
            data = nullptr;
        }
    }

    // 복사 대입 연산자
    MyString& operator=(const MyString& other) {
        cout << "복사 대입 연산자 호출" << endl;
        if (this != &other) {
            delete[] data;

            if (other.data != nullptr) {
                data = new char[strlen(other.data) + 1];
                strcpy(data, other.data);
            } else {
                data = nullptr;
            }
        }
        return *this;
    }

    // 이동 생성자
    MyString(MyString&& other) noexcept : data(nullptr) {
        cout << "이동 생성자 호출" << endl;
        data = other.data;
        other.data = nullptr;
    }

    // 이동 대입 연산자
    MyString& operator=(MyString&& other) noexcept {
        cout << "이동 대입 연산자 호출" << endl;
        if (this != &other) {
            delete[] data;
            data = other.data;
            other.data = nullptr;
        }
        return *this;
    }

    // 기본 소멸자
    ~MyString() {
        cout << "소멸자 호출" << endl;
        delete[] data;
    }

    // 문자열 설정
    void setData(const char* str) {
        delete[] data;
        data = new char[strlen(str) + 1];
        strcpy(data, str);
    }

    // 문자열 출력
    void printData() const {
        if (data != nullptr) {
            cout << "데이터: " << data << endl;
        } else {
            cout << "데이터가 없습니다." << endl;
        }
    }
};

int main() {
    // 기본 생성자 호출
    MyString str1;

    // 문자열 설정
    str1.setData("Hello");

    // 복사 생성자 호출
    MyString str2 = str1;

    // 복사 대입 연산자 호출
    MyString str3;
    str3 = str1;

    // 이동 생성자 호출
    MyString str4 = std::move(str1);

    // 이동 대입 연산자 호출
    MyString str5;
    str5 = std::move(str2);

    // 소멸자 호출
    // str1, str2, str3, str4, str5의 수명이 끝났을 때 호출됨

    return 0;
}

 

 

복사 생성자와 대입연산자 오버라이딩

기본 복사 생성자와 복사 대입 연산자에는 문제가 있다. 바로 얕은 복사가 이루어진다는 것이다.

즉, 하나의 클래스 객체가 다른 클래스 객체에게 할당되면 두 객체가 동일한 메모리 주소 값을 가진다. (CallByAddress와 동일한 현상이다.)

 

하지만, 대부분의 상황에서는 복사가 이루어진다면 내부의 값을 복사한 완전 다른 객체를 원할 것이다. (CallByValue와 동일하다.)

 

하여, 얕은 복사를 하면 두 객체가 같은 메모리를 가리켜 한쪽이 해제시 문제가 발생 할 수 있다. 그래서 깊은복사를 하도록 재정의 해야 합니다.

 

복사 이동
복사 = 주소 값과 동일한 값 할당
복사는 메모리 할당과 내부 값을 복사하여 새로운 객체를 생성
이동 = 주소 값의 소유권 변경
이동은 소유권을 가져가기에 가르키고 있는 주소만 변경

 

 


참고자료

 

[C++] 멤버 초기화 리스트 (member initializer lists)

안녕하세요. BlockDMask 입니다. 오늘은 C++ 멤버 초기화 리스트 라는 주제로 이야기를 해보려합니다. 1. 멤버 초기화 리스트란? 2. 멤버 초기화 리스트를 꼭 사용해야하는 경우 1. C++ member initializer lis

blockdmask.tistory.com

 

 

[C++] 생성자와 초기화 리스트 (Initializer List)

C++에서 생성자는 어떤 구조체 또는 클래스 객체의 생성 시 자동으로 호출되는 함수이다. 따라서 생성자에서는 흔히 초기화에 필요한 동작을 수행하게 되는데, 이때 멤버 변수를 초기화하는 데

pandas-are-bears.tistory.com

 

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함