Part2::Ch 01. 클래스 - 03. 생성자(Constructor)

2025. 4. 29. 17:42Programming Language/C++

반응형

1. 생성자란

생성자는 객체가 생성될 때 자동으로 호출 되어 멤버 변수들을 초기화해주는 특수한 함수이다.

생성자는 클래스와 이름이 같고 반환형이 없으며 객체가 만들어질때 자동으로 호출된다.

 

클래스는 이전에 봤던것과 같이 멤버 변수는 private로 제한하여 외부에서 멤버 변수에 접근할수 없도록 만드는 것이 보통이다.

또한 만약 public으로 생성해서 만들어준다고 하더라도 

class Car {
public:
    int speed;
}

int main() {
    Car car;
    std::cout << car.speed << std::endl;
}

이렇게 구현되어 있을때 speed는 초기 값이 들어가 있지 않기 때문에 호출하면 쓰레기 값이 출력되게 된다.

 

그래서 객체를 만들자마자 자동으로 초기값을 설정해주는 함수가 필요했기에 나온 것이 생성자이다.

 

2. 생성자의 기본 문법

생성자를 만드는 기본적인 방법은

class 클래스명{
private:
	int member;
    std::string member2;
    float member3;
public:
    클래스명(){
        //private로 관리되는 멤버들의 초기화 코드
        member = 0;
        member2 = "";
        member3 = 0;
    }
}

와 같이 만들고 실제로 사용하는 예시를 보자면

class Car{
private:
    int speed;
public:
    Car(){
        speed = 0;
    }
}

과 같이 만들어주고 이를 메인함수에서 객체로 생성하게 된다면

int main(){
    Car car;  //=> 이 시점에 Car의 생성자가 자동으로 호출된다
    std::cout << car.speed << std::endl; // 0이 출력됨
}

 

3. 생성자의 종류

생성자는 호출될때 용도에 따라서 디폴트 생성자, 매개변수 생성자, 복사 생성자로 분류된다.

 

1) 디폴트 생성자

디폴트 생성자는 아무런 매개변수를 갖지 않은 생성자로 만약 사용자가 직접 생성자를 정의하지 않았을 경우 컴파일러에 의해서 만들어지는 생성자이다.

class Car{
private:
    int speed;
public:
};

이렇게 클래스를 구현했을때 객체를 하나 생성하면

기본적으로 디폴트 생성자를 호출한다.

물론 저렇게 구현자체가 되어 있지 않다면 그 객체의 멤버 변수에는 쓰레기 값이 들어가 있게 된다.

여기서 사용자가 만약에 디폴드 생성자를 직접 설정하는 순간 

컴파일러는 더이상 기본 생성자를 생성해주지 않는다.

그리고 이제 객체를 생성할때 우리가 생성한 디폴트 생성자를 호출하게 된다.

여기서 우리는 각 멤버 변수의 초기값을 지정하고 

이제 객체를 생성해서 멤버변수를 출력해보면

이렇게 멤버변수를 초기화해주는 것을 볼 수 있다.

2) 매개변수를 가진 생성자

생성자에는 직접 매개변수를 받아 멤버변수를 초기화하는 것이 가능하다.

이때는 사용자가 객체를 생성할때 무조건 멤버변수의 값을 전달하면서 생성해줘야 만한다.

그 이유는 사용자가 생성자를 직접 선언하지 않는다면 디폴트 생성자를 컴파일러에서 자동으로 만들어 줬고 그 생성자는 아무런 매개변수를 받지 않는 생성자였기에 때로 값을 전달해서 초기화를 하는 행위를 하지 않았기 때문이다.

 

그런데 사용자가 매개변수를 가지는 생성자를 선언한 순간부터 컴파일러는 자동으로 디폴트 생성자를 생성하지 않게 되고 그럼에 따라서 

이렇게 매개변수를 던지지 않았을때 호출할 생성자가 존재하지 않아서 에러를 만드는 것이다.

 

그렇기에 

이렇게 생성자의 매개변수에 맞게 전달인자를 전달해야만 한다.

 

물론 매개변수가 있는 생성자와 매개변수가 없는 디폴트 생성자를 동시에 선언한다면 

객체를 생성할때 매개변수를 받는지 안받는지에 따라서 컴파일러가 선택적으로 생성자를 부를 수 있도록 해주게 되면서

이 두 방식 모두 사용이 가능하게 된다.

여기서 만약에 

이렇게 값을 할당하는 디폴트 생성자를 쓴다면 이걸 사실은

이렇게 디폴트 값을 추가해줌으로써

하나의 생성자로써 디폴트 생성자 + 매개변수를 받는 생성자 두가지 역할을 수행할 수 있게 된다.

오히려 이렇게 작성해준 다음부터 

이렇게 디폴트 생성자를 호출하는 객체 생성파트에서 디폴트 생성자가 두개라고 나오는 에러가 발생할 수 있다.

왜냐면 결국엔 매개변수가 없이 객체를 생성할때 어떤 생성자를 봐야할지 모르게 되기 때문이다.

 

그래서 저렇게 매개변수를 사용하는 생성자에서 기본값을 지정한다면 만들어둔 디폴트 생성자는 제거해줘야 한다.

 

여기서 추가적으로 

이렇게 Car 기본생성자에서 speed의 값을 10으로 생성하고 매개변수를 갖는데 기본값을 지정한 생성자가 존재 할때 추가로 생성자 위임이라는 것을 사용할 수 있는데 이는 A 생성자가 B 생성자의 기능을 모두 가진다고 가정 했을때 B 생성자를 A에서 호출함으로써 A에서는 B 생성자의 기능을 코드로 작성하지 않게 할 수 있는 기능이다.

 

위와 같은 경우는 매개변수를 가지는 생성자의 매개변수에 10을 전달하면서 생성자를 호출한다면 기본 생성자의 기능을 매개변수를 가진 생성자가 대신 해줄 수 있게 된다.

 

그렇기에 디폴트 생성자에서 매개변수 있는 생성자에 10을 전달하면서 호출하면 그 기능에 대한 코드를 작성할 필요가 없을텐데 이걸 코드로써 보여주자면 

이렇게 선언해주면 된다.

 

그리고 매개변수가 있는 생성자에서

Car(int _speed) {
    speed = _speed;
    std::cout << "Created Parameter Constructor" << std::endl;
}

이렇게 구현되기를 바란다면 궂이 speed = _speed라고 작성하는 것을 대신해서

이렇게 

Car(int 매개변수명1, float 매개변수명2..)
    : 멤버변수명1(매개변수명1), 멤버변수명2(매개변수명2)..
{

}

선언해주면 자동으로 매개변수 1은 멤버변수 1에 초기화되고 매개변수 2는 멤버변수 2에 초기화 되게 된다.

 

이런 방식을 멤버 이니셜라이져 리스트, 멤버 초기화 목록 이라고 한다.

 

이 방식의 장점은 대입 방식을 사용하면 멤버변수가 생성되고 나서 대입을 진행하지만 위 멤버 이니셜라이져 리스트를 사용하게 되면 멤버변수를 생성하면서 초기화하기 때문에 한 단계를 건너 뛰어 성능상 이점을 챙길수 있고 추가로 멤버 변수가 const로 선언되어 있는 경우는 대입이 불가능하고 초기화가 무조건 필요하기 때문에 멤버 이니셜라이져 리스트를 사용할 수 밖에 없게된다.

 

  • : speed(_speed)
    "speed가 태어날 때 바로 값을 넣는다"
    (초기화 단계에서 값 지정)
  • { speed = _speed; }
    "speed를 먼저 만든 다음, 따로 값을 넣는다"
    (생성 후 대입)

추가로 멤버 이니셜라이져 리스트에서 초기화 되는 순서는 멤버변수의 순서대로로 

class Car {
private:
	int speed;
	int fuel;
	int plate;
public:
	Car(int _speed, int _fuel, int _plate)
		:plate(_plate), speed(_speed), fuel(_fuel)
	{
		std::cout << "Created Parameter Constructor" << std::endl;
	}
}

이렇게 선언했다고 해서 plate → speed → fuel 순이 아니라 무조건 speed → fuel → plate 순으로 초기화가 되게 된다.

 

이 순서가 중요한 경우도 있다고하니 알아두고 넘어가도록 하자.

 

3) 복사 생성자

자신과 같은 타입의 객체를 이용해서 새로운 객체를 초기화할 때 호출되는 특수한 생성자로 기본적인 형태는

클래스이름(const 클래스이름& 매개변수) { }

와 같이 구성되는데 이때 반드시 참조(&)를 받아야하고 원본 객체가 변경되지 않도록 const로 받는게 기본이다

 

이 생성자가 호출되는 시점은 객체를 다른 객체로 초기화하거나( Car b = a ), 객체를 함수의 인자로 넘길 때(값으로, func(Car a)), 함수가 객체를 값으로 반환할 때(return a) 호출된다.

 

복사 생성자를 만들지 않은 경우

복사생성자를 만들지 않으면 컴파일러가 기본 복사 생성자를 자동으로 생성해주나 기본 복사의 경우는 모든 멤버를 단순히 비트단위로 복사를 하여 객체 안의 멤버 변수들이 가지고 있는 메모리의 값을 그대로 복사한다(Shallow Copy - 얕은 복사).

이때 기본적인 타입(int ,float 등등..)의 경우는 값 자체를 복사한다고 해서 문제가 있지 않으나 포인터나 동적 메모리를 갖는 경우 문제가 발생할 수 있다.

class A {
public:
    int x;
};

A a;
a.x = 10;
A b = a;  // b.x = 10

이렇게 값 자체를 복사해오는게 기본형의 경우는 객체 a의 멤버 x의 값과 객체 B의 멤버x의 값이 완전 독립된 값이되기 때문에 문제가 되지 않는다.

그러나 포인터의 경우는 복사해온 값이 어떤 메모리 공간을 가리키는 주소값이기 때문에 

class B {
public:
    int* ptr;
};

B a;
a.ptr = new int(100);

B b = a;  // 얕은 복사

객체 a의 멤버변수 ptr과 객체 b의 멤버변수 ptr이 가리키는 메모리 공간이 동일한 메모리 공간이 된다.

 

이럴때 객체 a의 ptr와 객체 b의 ptr의 조작이 동일한 메모리 공간에서 이뤄지기 때문에 데이터의 신뢰성이 떨어지게 된다.

또한 ptr이 위와 같이 동적 할당된 값을 가리킬때 a의 ptr에서는 뎅글러 포인트가 되지 않게 만들어졌다고 하더라도 객체 b의 ptr에는 그렇게 뎅글러 포인터에 대한 대첵을 만들지 않은 경우엔 문제가 발생하게 된다.

delete a.ptr;   // a.ptr 메모리 해제
*b.ptr = 5;     // ❌ b.ptr은 죽은 메모리를 참조 중 → 터진다

 

그래서 복사 생성자를 직접 선언해줘야만 이런 문제들에서 자유로울 수 있다.

 

그래서 

이런식으로 일반 변수의 경우는 값을 전달하고, 포인터의 경우는 동적할당을 사용해서 ptr의 값을 메모리공간을 만들고 직접 할당해서 ptr에 저장해줘야만 복사가 된다.

 

* default와 delete

Car()
{
}

Car(int _speed)
    :speed(_speed)
{
    std::cout << "Created Parameter Constructor" << std::endl;
}

이렇게 선언한 경우에 디폴트 생성자에는 중괄호를 지우고 싶다면 default 키워드를 사용하면 된다.

Car() = default;

Car(int _speed)
    :speed(_speed)
{
    std::cout << "Created Parameter Constructor" << std::endl;
}

 

추가로 만약 내가 원하는 방식이 디폴트 생성자가 만들어지지 않기를 바란다면 default 대신에 delete를 넣어주면

Car() = delete;

Car(int _speed)
    :speed(_speed)
{
    std::cout << "Created Parameter Constructor" << std::endl;
}

 

이제 디폴트 생성자는 사용하지 못하게 된다.

 

이 또한 사용자의 의도에 따라서 사용할수도 있는 방법이니 인지하고 있자.

 

반응형