객체지향프로그래밍(5) - 생성자

기본적인 생성자에 대한 이해

Featured image

🔚 짧게 하는 복습

✅ 1. 캡슐화가 무엇인지 알고, 그 장점이 무엇인지 안다.

✅ 2. 추상화가 무엇인지 알고, 그 장점이 무엇인지 안다.

혹시 기억이 안 난다면, 다시 돌아가자


저번 강의에서는 왜 객체지향 프로그래밍이 필요한지, 캡슐화추상화를 통해 어떤 목적을 이룰 수 있는지에 대해 다루었다.

이번 강의에서는 객체지향의 생성자에 대해 다루어보겠다.


현재까지 클래스의 문제

저번 시간까지 다루던 포켓몬 코드를 가져와 보겠다.

#include <iostream>
#include <string>

enum { Fire, Water, Grass };

struct skill_info {
	int power;
	int accuracy;
};

class pokemon {
private:
	// pokemon의 private 변수
    std::string name;
	int type;
	double hp;
	int attack;
	int defend;
	int mobility;
	struct skill_info skill;
};// pokemon 클래스

int main() {
	pokemon pomon = { "브케인", Fire, 450, 45, 10, 20, {8, 50} };
}

우리는 이 코드가 왜 안되는지에 대해서 배운 적이 있다.

pokemon의 내부 변수는 외부 참조를 통해서만 접근할 수 있어서, 구조체처럼 선언할 수 없다고 했다.

그럼 어떻게 해야 할까? 이런 방식을 고안해낼 수 있다.

class pokemon {
private:
	// pokemon의 private 변수
    std::string name;
	int type;
	double hp;
	int attack;
	int defend;
	int mobility;
	struct skill_info skill;
public:
    void get_name(std::string _name){
        name = _name
    }
    void get_type(int _type){
        type = _type
    }
    // 같은 과정 반복..

};// pokemon 클래스

그런데 어떤가? 보기만 해도 화가 나지 않는가?

포켓몬 속성이 겨우 10개도 안 돼서 다행이지, 100개였으면 초기화만 하다가 코드가 한 페이지를 넘어갈 것이다.

이러한 문제를 해결하기 위해서, 객체지향 프로그래밍에서는 초기화를 용이하게 해주는 생성자를 제공한다.


생성자란?

생성자객체 생성 시 호출하여 초기화를 하는 메소드이다.

생성자의 이름은 무조건 클래스 이름과 같다.

특이한 점으로는 생성자는 반환형이 없고 객체.생성자() 이렇게 사용하는 것이 아니라, 마치 그냥 C언어의 함수처럼 사용된다.

즉, 클래스 이름() 이렇게 호출하면 된다는 뜻이다.

생성자에 대해서 특별한 선언을 하지 않으면, 디폴트 생성자라는 것이 암묵적으로 생성된다.

그리고 그 디폴트 생성자가 호출되면, 임의의 값이 들어간 채로 초기화된다.

아래의 코드를 직접 보자.


실습 코드

#include<iostream>

enum { Fire, Water, Grass };

struct skill_info {
	int power;
	int accuracy;
};


class pokemon {
private :
    // pokemon 클래스의 private 변수
	std::string name;
	int type;
	double hp;
	int attack;
	int defend;
	int mobility;
	struct skill_info skill;

public:
    // pokemon 클래스의 public 메소드
	void get_info() {
		std::cout << "이름 : "<< name << std::endl;
		std::cout << "타입 : "<< type << std::endl;
		std::cout << "체력 : "<< hp << std::endl;
		std::cout << "공격력 : "<< attack << std::endl;
		std::cout << "방어력 : "<< defend << std::endl;
		std::cout << "이동속도 : "<< mobility << std::endl;
		std::cout << "스킬 위력 : "<< skill.power << std::endl;
		std::cout << "스킬 명중률 : "<< skill.accuracy << std::endl;
	}
}; //pokemon 클래스

int main(){
	pokemon pomon1 = pokemon(); // 디폴트 생성자
	pokemon pomon2; // 위의 코드와 같이 디폴트 생성자 호출

	pomon1.get_info();
	pomon2.get_info();
}

결과

이름 :
타입 : 0
체력 : 0
공격력 : 0
방어력 : 0
이동속도 : 0
스킬 위력 : 0
스킬 명중률 : 0
이름 :
타입 : -858993460
체력 : -9.25596e+61
공격력 : -858993460
방어력 : -858993460
이동속도 : -858993460
스킬 위력 : -858993460
스킬 명중률 : -858993460

미리 알려준 대로, 임의의 값이 들어간 것을 알 수 있다.

그렇다면 생성자는 어떻게 수정할 수 있을까?


생성자 수정

암묵적으로 선언된 생성자를 명시적으로 생성 후, 직접 값을 수정해주면 된다.

클래스 내에 아래의 구문을 추가해보자.

pokemon() {
		name = "Unknown";
		type = -1;
		hp = -1;
		attack = -1;
		defend = -1;
		mobility = -1;
		skill = { -1, -1 };
	}// 특이값으로 초기화

그리고 다시 실행해보자.

이름 : Unknown
타입 : -1
체력 : -1
공격력 : -1
방어력 : -1
이동속도 : -1
스킬 위력 : -1
스킬 명중률 : -1
이름 : Unknown
타입 : -1
체력 : -1
공격력 : -1
방어력 : -1
이동속도 : -1
스킬 위력 : -1
스킬 명중률 : -1

우리가 설정해준 초깃값에 맞게 초기화되었다.

즉, 우리는 기본적인 생성자를 수정하고 그를 통해 초기화하는 방법에 대해서 알 수 있다.

이렇게 생성자는 아무런 매개변수 없이 호출될 수도 있지만, 사실 매개변수를 받아서 만들 수도 있다.


매개변수를 통한 생성자

게임에서도 포켓몬의 이름을 정하는 기능이 있다.

우리는 원하는 이름을 미리 입력받아, 그 이름을 가진 포켓몬으로 만들어주는 코드를 작성해보자.

	pokemon(std::string _name) {
		name = _name;
		type = -1;
		hp = -1;
		attack = -1;
		defend = -1;
		mobility = -1;
		skill = { -1, -1 };
	}//이렇게 바꿔줌

    //... 생략

    int main() {
	pokemon pomon = pokemon("꼬북이"); // 매개변수를 통한 생성자

	pomon.get_info();
}
이름 : 꼬북이
타입 : -1
체력 : -1
공격력 : -1
방어력 : -1
이동속도 : -1
스킬 위력 : -1
스킬 명중률 : -1

이번에도 잘 되는 모습이다. 이제 어떠한 입력을 받아서 만들 수 있다는 것도 알았다.

마지막으로는 모든 속성을 입력받아보자.

“브케인”, Fire, 450, 45, 10, 20, { 8, 50 }로 입력을 받아서, 이 능력치대로 브케인을 만들어주는 생성자를 만들어보자.

pokemon(std::string _name, int _type, double _hp, int _attack, int _defend, int _mobility, struct skill_info _skill) {
		name = _name;
		type = _type;
		hp = _hp;
		attack = _attack;
		defend = _defend;
		mobility = _mobility;
		skill.power = _skill.power;
		skill.accuracy = _skill.accuracy;
	};

이렇게 바꿔주면 되겠다. 그런데 이거 생각보다 너무 귀찮다.

속성이 훨씬 많다고 생각해보면, 한눈에 알아보기도 너무 어려워진다.

그래서 C++에서는 생성자 리스트라는 개념이 도입된다.


생성자 리스트

위의 생성자를 아래와 같이 파격적으로 줄일 수 있다.

pokemon(std::string _name, int _type, double _hp, int _attack, int _defend, int _mobility, struct skill_info _skill) : name(_name), type(_type), hp(_hp), attack(_attack), defend(_defend), mobility(_mobility), skill(_skill) {}

이 같은 양식의 생성자를 생성자 리스트를 통한 생성자라고 한다.

꼭 모든 속성을 설정해줄 필요는 없고, 필요한 부분만 기본적으로 생성시킬 수도 있다.

pokemon(std::string _name) : name(_name){}// 이름만 기본적으로 생성


pokemon(double _hp, int _attack, int _defend, int _mobility, struct skill_info _skill) :hp(_hp), attack(_attack), defend(_defend), mobility(_mobility), skill(_skill) {} // 능력치만 기본적으로 생성

사실 뭐가 바뀐 것인지 체감이 안 될 법한데 이는 추후에 여러 개념이 확장될 때 빛을 발한다.

여전히 길긴 하지만, 어떤 값이 어디에 대입되는지 전보다는 읽기 쉽다.

또, 그렇기에 수정하기도 쉬워져서 유지보수에도 도움이 된다.

그리고 생성과 대입이 한 번에 일어나서, 속도 측면에서도 이점이 있다.


📖 오늘의 핵심(다 알기 전까지는 넘어가지 말자❗)

✅ 1. 디폴트 생성자가 무엇인지 안다.

✅ 2. 매개변수를 통한 생성자를 만들 수 있다.

✅ 3. 생성자 리스트를 통한 생성자를 만들 수 있다.

⚠️ 생성자 리스트에서 꼭 매개변수로 입력된 값만 사용할 수 있는 것이 아니다.

예를 들어,

pokemon() : name("unknown"), type(0), hp(0), attack(0), defend(0), mobility(0), skill({0,0}) {} //디폴트 생성자

💣 과제,

  1. 포켓몬의 이름, 공격력만 입력받은 값으로 설정시키는 생성자를 생성해보자.(난이도 下)