[C++] 생성자(Constructor)

SuLee·2021년 8월 10일
0

C++

목록 보기
3/8

📌 1. 생성자(Constructor)

생성자(Constructor)는 객체를 생성할 때 자동으로 호출되는 함수로 멤버 변수를 초기화하는 역할을 한다.

생성자를 정의할 때, 값을 반환하지 않지만 void를 따로 붙여주지 않는다. 또한 생성자의 이름은 클래스의 이름과 동일하게 짓는다.

생성자 예시

class Date {
private:
    int year;
    int month;
    int day;
public:
    Date() // 디폴트 생성자
    {
        year = 2021;
        month = 8;
        day = 10;
    }
    Date(int _year, int _month, int _day) // 생성자
    {
        year = _year;
        month = _month;
        day = _day;
    }
    Date(const Date& dt) // 복사 생성자
    {
        year = dt.year;
        month = dt.month;
        day = dt.day;
    }
};

예시와 같이 디폴트 생성자와 매개변수를 가지는 생성자를 정의하면 오버로딩을 통해 원하는 생성자를 호출할 수 있다.

클래스의 객체를 생성할 때 어떻게 작성하는지에 따라 암시적 방법과 명시적 방법으로 나뉜다.

Date day(2021, 8, 10); : 암시적 방법
Date day = Date(2021, 8, 10); : 명시적 방법


📌 2. 디폴트 생성자(Default Constructor)

만약 객체를 생성할 때, 생성자를 정의하지 않거나 생성자에 인수를 전달하지 않는다면 디폴트 생성자가 호출된다. 디폴트 생성자는 인자가 없는 생성자로, 클래스 내부에 생성자가 정의되어 있지 않을 때 컴파일러가 자동으로 추가해주는 생성자이다. 만약 디폴트 생성자가 명시되어 있지 않은 상태에서 객체를 생성하면 할당 전까지 쓰레기값을 가진다.

class Date {
private:
    int year;
    int month;
    int day;
public:
    void printYear() { cout << year << endl; }
};

int main() {
    Date date;
    date.printYear()
}

출력

-858993460

클래스 내부에 디폴트 생성자를 따로 정의할 수도 있다.

class Date {
private:
    int year;
    int month;
    int day;
public:
    Date() // 디폴트 생성자
    {
        year = 2021;
        month = 8;
        day = 10;
    }
    void printDate()
    {
        cout << year << endl;
        cout << month << endl;
        cout << day << endl;
    }
};

int main() {
    Date date;
    date.printDate()
}

출력

2021
8
10

📌 3. 복사 생성자(Copy Constructor)

복사 생성자를 알기 전에 먼저 복사의 종류를 알아야 한다. 복사에는 얕은 복사, 깊은 복사 두 종류가 있다.

얕은 복사는 값 자체를 복사하는 것이 아닌 주소값을 복사한다. 때문에 메모리에 인스턴스가 새로 생성되지 않고 본래 객체의 메모리를 가리킨다.

class Test {
public:
    char* name;
    Test() {}
    ~Test() {} // 소멸자
    void PrintInfo() { cout << name << endl; }
};

int main() {
    Test t1;
    t1.name = new char[3];
    strcpy(t1.name, "AB");
    t1.PrintInfo();
 
    Test t2 = t1;
    strcpy(t1.name, "AC");
    t1.PrintInfo();
    t2.PrintInfo();
}

출력

AB
AC
AC

위와 같이 원래 가리키고 있던 객체 t1name의 주소값이 변경되었을 때 얕은 복사를 통해 복사된 객체 t2name도 같이 변경된 것을 알 수 있다.

얕은 복사의 문제점은 클래스 내부에 동적으로 메모리를 할당한 경우에 있다. 이 경우, 서로 다른 두 개의 포인터가 하나의 메모리를 가리키게 된다. 이 때 하나의 객체의 소멸자가 호출됨과 동시에 메모리를 해제하게 되면 나머지 하나의 객체에서 이미 해제된 메모리에 접근하게 되면서 오류가 발생한다.

이와 달리 깊은 복사는 데이터 전체를 복사하고 새로운 메모리를 할당하여 원본과 다른 메모리를 차지하게 된다. 때문에 하나의 객체에서 소멸자를 호출해 메모리를 해제해도 다른 객체에는 영향이 없다.

만약 T라는 클래스가 있다고 한다면, 복사 생성자를 다음과 같이 정의할 수 있다.

T(const T& a);

아래와 같이 복사 생성자에서 num과 같은 변수는 얕은 복사를 수행하고, name과 같은 포인터 변수는 깊은 복사를 수행한다.

class Test {
public:
    int num;
    char* name;
    Test() {}
    Test(const Test& t) // 원본의 수정을 막기 위해 const 사용
    {
        num = t.num;
        name = new char[strlen(t.name) + 1];
        strcpy(name, t.name);
    }
    ~Test() // 소멸자
    {
        if (name) { delete[] name; }
    }
    void PrintInfo() { cout << name << endl; }
};

int main() {
    Test t1;
    t1.name = new char[3];
    strcpy(t1.name, "AB");
    t1.PrintInfo();
 
    Test t2 = t1;
    strcpy(t1.name, "AC");
    t1.PrintInfo();
    t2.PrintInfo();
}

출력

AB
AC
AB

0개의 댓글