객체 지향
우리가 이전 포스팅부터 지금까지 했던 방식은 절차(procedural) 지향 프로그래밍이다.
procedural은 함수이다. 함수 흐름으로 프로그램이 동작하는 방식이 절차 지향 프로그래밍이다.
C++은 객체 지향 언어인가?? 완전히 객체 지향 언어라고 보긴 어렵다.(C#이나 JAVA에 비해)
객체 지향의 주인공은 객체이다. 객체란? 플레이어, 몬스터, GameRoom 등 특정 오브젝트를 말한다고 보면 된다.
객체 지향에서는 데이터를 가공하는데 있어 객체를 기준으로 코드를 작성하는 것이다.
Knight를 설계해보자.
- 속성(데이터) : hp, attack, y, x...
- 기능(동작) : Move, Attack, Die...
이러한 것들을 가지려면 설계도가 필요하다.
바로 Class 이다.
#include <iostream>
using namespace std;
class Knight
{
public:
// 멤버 함수 선언
void Move(int y, int x);
void Attack();
void Die()
{
hp = 0;
cout << "Die" << endl;
}
public:
// 멤버 변수
int hp;
int attack;
int posY;
int posX;
private:
};
void Knight::Move(int y, int x)
{
}
void Knight::Attack()
{
cout << "Attack" << attack << endl;
}
이런 식의 설계도를 작성할 수 있다.
선언하는 방식은 구조체 변수를 선언하는 방식이랑 동일하다.
int main()
{
Knight knight;
knight.hp = 100;
knight.attack = 10;
knight.posY = 0;
knight.posX = 0;
return 0;
}
그렇다면 구조체와의 차이는 무엇이냐. 바로 멤버 함수의 유무라고 볼 수 있다.
생성자와 소멸자
class를 작성하기만 하면 메모리에 바로 올라가는 것은 아니다. 동작할당을 해 힙 영역에 올리거나 따로 스택에 할당을 해 주어야 한다.
클래스에 소속된 함수들을 멤버 함수라고 한다.
이 중에서 굉장히 특별한 함수 2종이 있는데, 바로 [시작]과 [끝]을 알리는 함수들이다.
생성자는 여러개가 존재 가능하며 소멸자는 오직 1개만 존재한다.
#include <iostream>
using namespace std;
class Knight
{
public:
// [1] 기본 생성자 (인자가 없음)
Knight()
{
cout << "Knight() 기본 생성자 호출" << endl;
}
// 소멸자 (오직 1개 존재)
~Knight()
{
cout << "~Knight() 소멸자 호출" << endl;
}
// 멤버 함수 선언
void Move(int y, int x);
void Attack();
void Die()
{
hp = 0;
cout << "Die" << endl;
}
public:
// 멤버 변수
int hp;
int attack;
int posY;
int posX;
private:
};
생성자는 메모리에 할당 되는 순간 자동으로 컴파일러에서 호출해준다.
소멸자는 객체가 사라질 때, 호출이 된다.
생성자에서는 대부분 멤버 변수의 값을 초기화 하는 역할을 한다. 물론 이런 역할만 하는 것은 아니지만 대표적으로 그러하다.
생성자는 여러개가 존재할 수 있다고 했는데 다음과 같다.
#include <iostream>
using namespace std;
class Knight
{
public:
// [1] 기본 생성자 (인자가 없음)
Knight()
{
cout << "Knight() 기본 생성자 호출" << endl;
}
// [2] 복사 생성자
// 생성자는 생성자인데 자기자신을 참조값으로 받음
// 다른 객체의 데이터를 똑같이 지닌 객체를 생성
Knight(const Knight& knight)
{
hp = knight.hp;
//...
}
// [3] 기타 생성자
Knight(int hp)
{
this->hp = hp;
}
// 소멸자 (오직 1개 존재)
~Knight()
{
cout << "~Knight() 소멸자 호출" << endl;
}
// 멤버 함수 선언
void Move(int y, int x);
void Attack();
void Die()
{
hp = 0;
cout << "Die" << endl;
}
public:
// 멤버 변수
int hp;
int attack;
int posY;
int posX;
private:
};
void Knight::Move(int y, int x)
{
}
void Knight::Attack()
{
cout << "Attack" << attack << endl;
}
int main()
{
Knight knight1;
Knight knight2(100);
Knight knight3(knight1);
return 0;
}
우리가 생성자를 명시적으로 만들지 않으면, 아무 인자도 받지 않는 기본 생성자가 컴파일러에 의해 자동으로 만들어진다. 그러나 우리가 명시적으로 아무 생성자 하나를 만들면 그 것을 호출하게된다.
상속성
객체 지향의 3대장으로 첫 번째인 상속성에 대해 알아보자.
우리가 위에 기사 클래스를 만들었다. 하지만 게임에 종족이나 직업이 기사 하나만 있지 않다. 법사나 궁수 등 여러가지 직업이 있을 것이다. 그렇다면 Mage라는 클래스를 만들고 기사 클래스에 있는 값들을 복사해 Mage에 적으면 해결 될 것이다.
그런데 코드가 되게 비슷한 부분이 많을 것이고 공통된 부분도 정말 많을 것이다. 이걸 해결하기 위한 것이 바로 상속이다.
상속은 자식에게 유산을 물려주는 것이나 다름 없다.
최 상위 객체를 만들어서 공통된 기능을 묶어서 그걸 나눠주는 형식인 것이다.
#include <iostream>
using namespace std;
class Player
{
public:
void Move(int y, int x) { }
void Attack() { cout << "Player Attack 호출" << endl; }
void Die() { }
public:
int hp;
int attack;
int posY;
int posX;
};
class Knight : public Player
{
public:
};
class Mage : public Player
{
public:
};
int main()
{
Knight k;
k.Attack();
return 0;
}
우리가 player를 상속했기 때문에 Knight에 함수가 없어도 Attack 함수를 호출할 수 있게 된다.
상속을 한 후 생성자와 소멸자도 매우 중요하다.
생성자는 탄생을 기념해서 호출되는 함수라고 생각해보면 Knight를 생성하면 Player의 생성자를 호출해야하나, Knight의 생성자를 호출해야하나 고민할 것이다.
#include <iostream>
using namespace std;
class Player
{
public:
Player(){
cout << "Player 생성자" << endl;
}
~Player()
{
cout << "Player 소멸자" << endl;
}
void Move(int y, int x) { }
void Attack() { cout << "Player Attack 호출" << endl; }
void Die() { }
public:
int hp;
int attack;
int posY;
int posX;
};
class Knight : public Player
{
public:
Knight()
{
cout << "Knight 생성자" << endl;
}
~Knight()
{
cout << "Knight 소멸자" << endl;
}
};
class Mage : public Player
{
public:
};
int main()
{
Knight k;
k.Attack();
return 0;
}
결론은 둘 다 호출이 된다.
순서가 매우 중요하다.
Knight의 생성자가 호출이 되고 그 생성자의 선 처리 영역에서 Player 생성자를 호출하게 된다. 그리고 Player의 생성자 코드가 작동 한 후 Knight의 생성자 코드가 호출된다.
즉, 부모가 먼저 생성자 호출하고 자식이 생성자 호출하는 것처럼 보인다. 하지만 정확히 말하면 그렇진 않다.(선 처리 영역 덕분)
소멸자는 Kngiht의 소멸자가 호출되고 그 코드가 동작한 후, 후 처리 영역에서 Player의 소멸자를 호출하게 된다.
'C++ > 기초' 카테고리의 다른 글
[C++] 객체 지향 - 3 (0) | 2023.11.27 |
---|---|
[C++] 객체 지향 - 2 (1) | 2023.11.27 |
[C++] 포인터 - 4 (다중 포인터, 다차원 배열) (0) | 2023.11.26 |
[C++] 포인터 - 3 (배열) (1) | 2023.11.25 |
[C++] 포인터 - 2 (참조) (1) | 2023.11.25 |