강의보고 공부한 것을 정리하는 목적으로 작성된 글이므로 틀린점이 있을 수 있음에 양해부탁드립니다.
(피드백 환영입니다)
일단 객체지향을 공부하기 전에 객체지향이 왜 필요한지에 대해 알아야 한다고 생각한다.
모든 프로그램은 데이터 + 로직
으로 작동이 된다.
그러면 객체지향 없이 사용해도 뭐 상관없지 않냐? 라고 할 수 있다.
물론 객체지향 프로그래밍
없이도 구현할 수는 있다.
하지만 코드가 급격하게 늘어난다면 객체지향 프로그래밍
을 하지 않았더라면
아무리 파일을 분리해서 관리한다고 하여도 관리를 하는것에 대한 한계가 있을 것이다.
한 번 절자지향적으로 간단한 코드를 적어본다면
#include <iostream>
using namespace std;
struct Knight
{
int hp;
int attack;
int defence;
};
void HealPlayer(Knight& a, int value)
{
a.hp += value;
}
int main()
{
Knight k1;
k1.hp = 100;
k1.attack = 10;
k1.defence = 3;
HealPlayer(k1, 100);
return 0;
}
만약 이러한 코드가 있을 때 문제점이 있을 수 있다.
일단 HealPlayer라는 함수
가 Knight라는 타입만 받아서 함수를 실행할 수 있다는 점이다.
만약 나중에 다른 직업인 Archer
, Wizard
같은 직업이 나온다면 함수를 다 따로 만들어 줘야하는 번거로움이 생긴다.
즉 함수의 사용이 한정된다는 단점이 생길 수 있다.
그리고 나중에 점점 코드가 커지다보면 함수만으로는 프로그램(게임)을 짜기 힘들어지는 순간이 올 것이다.
데이터를 만들고 로직을 짜는 순서에서 탈출해 모든것을 객체단위
로 생각한다고 보면 된다.(객체끼리의 상호작용이 '주'가 됨)
그럼 객체
가 무엇인가? 라는 생각을 할 수 있는데
객체의 정의
를 보면 메모리를 할당받아 프로그램에 사용되는 모든 데이터이다.
즉 우리가 사용했던 변수
, 구조체
, 함수
등이 전부 객체
라고 할 수 있다.
그렇지만 객체지향(opp)에서의 객체
는 대부분 클래스의 인스턴스
라고 생각하면 될 것 같다.
롤을 예로들어 설명하자면
롤에나오는 미니언
, 챔피언
, 오브젝트
등의 세부사항과 그것들이 하는 행동까지 포함한 것이 객체
라고 할 수 있다.
여기서 그럼 눈에 보이는 것만 객체인가? 라고 생각할 수 있는데
맵
, 로비
, 게임시작창
이런것들도 객체
라고 볼 수 있다.
다시 개념들을 정리해보자면
객체(object)
개념
특징
인스턴스
개념
특징
아래의 블로그의 글이 아주 잘 정리가 되어있어서 가져와서 적어보았다.
참조한 블로그
일반적으로 객체지향 프로그래밍을 할 때는 class
라는 것을 사용하기 때문에 class
를 사용해보자
여기서 여담으로 C++에서는 struct와 class의 차이가 별로 없기 때문에 struct를 사용해서 할 수도 있다.
하지만 c#이나 java로 가면 class는참조
, struct는값 방식
으로 둘이 아예 다른 것이 되어버리는 것으로 알고있다.
일단 class
를 사용해보자
class Knight
{
};
class
는 struct
를 사용하는 것 처럼 사용하면 된다.
class
의 정의를 알아볻자면 대충 객체를 생성하기 위한 설계도? 정도로 생각해주면 될 것 같다.
class
라는 설계도로 객체
를 만들어서 사용을 한다 라고 보면 된다.
그럼 안을 채워보자
class Knight
{
public:
public:
// 멤버 변수
int _hp;
int _attack;
int _defence;
};
class
안에 들어가는 데이터를 멤버 변수
라고 불러준다.
멤버변수
를 작성할 때 일반 변수
인지 멤버 변수
인지 헷갈릴 수 있기 때문에
프로젝트마다 네이밍컨벤션을 정해서 한다. 지금은 앞에 언더바(_)를 붙여준걸 볼 수 있다.
여기까지 보면 struct
를 사용할 때랑 다른점이 없다.
그리고 class
만 만들었을 때는 아직까지 어떠한 메모리 영역도 자치하지 않고 Knight로 객체
를 만들게 되면 Knight는 이러한 것들로 이루어져있다 라는 것을 알고 있어야 한다.
int main()
{
Knight k1;
}
이런식으로 객체
를 스택영역에 만들어줄 수 있다. (아직 동적할당은 하지 않았음으로)
그리고 객체
가 하는 행동(함수)까지 넣어서 만들어 줄 수 있다.
class Knight
{
public:
// 멤버 함수
void Attack() { cout << "Attack" << endl; }
void Die() { cout << "Die" << endl; }
void HealMe(int value)
{
_hp += 10;
}
public:
// 멤버 변수
int _hp;
int _attack;
int _defence;
};
일단 행동(함수)를 멤버 함수
라고 부른다.
그런데 여기서 이해가 안되는 부분이 있을 수 있을 것이다.
HealMe()
이 함수 부분에서 어떤식으로 hp를 받아들이는가? 이다.
한 번 하나씩 따라가보자
Knight라는 객체
가 만들어졌을 때 일부분(멤버 변수
)은 메모리에 할당이 될 것이고, 일부분(함수
)은 메모리가 할당되는 것이아닌 코드영역에 들어가는 것이다.
즉 위 코드로 보자면 hp
, attack
, defence
는 스택영역에 들어가지만 Attack()
, Die()
, HealMe()
같은 함수들은 코드로 변경이 되어 코드영역에 들어가게 된다고 볼 수 있다.
즉 Knight에 객체
를 100개 만들어도 함수부분이 100개씩 만들어지는 것은 아니다.
즉 Knight(class)
에 함수를 만든다면 그 Knight를 대상으로 실행한다고 생각을 하면 된다.
그리고 HealMe(int value) 함수
는
void HealMe(Knight* Kptr, int value)
{
// 자기자신의 포인터를 받음
Kprt->hp += value;
}
와 거의 유사하다고 볼 수 있다.(저 부분이 숨겨져있는 것이라고 생각하면 된다)
클래스
안에 들어가있으므로 그 클래스
에 멤버변수
를 포함해가지고 객체
가 만들어지기 때문에 함수 내부적으로 hp를 건드릴 수 있게 되는것이다.
그리고 멤버 함수
안에서 자기자신에 대한 주소를 this
라는 문법으로 사용할 수 있다.
void HealMe(int value)
{
// 자기자신의 포인터를 받음
this->hp += value;
}
즉 이런식으로 this
가 숨겨져있다고 보면 된다.
즉 객체
를 대상으로 함수가 실행된다라고 볼 수 있다.
객체지향
에서 가장 중요한 것은 멤버 변수
와 멤버 함수
라고 볼 수 있지만
나중에 또 다양한 문법들을 배우면서 멤버 변수
와 멤버 함수
를 더욱 편리하게 사용할 수 있게 될 것이다.
생성자
는 클래스
에 객체
가 만들어질 때 무조건 호출되는 함수
라고 볼 수 있다.
생성자
는 특이하게 함수
이지만 리턴타입이 없다 그리고 함수 이름을 클래스와 동일하게 만들어주면 된다.
그리고 또 소멸자
라는 것이있는데 이 소멸자
는 객체
가 소멸이 될 때 호출되는 함수
이다.
소멸자
도 특이한것이 함수
이지만 리턴타입이 없고 또 인수도 없다 그리고 함수 이름은 생성자
와 똑같이 클래스와 동일하게 만들어주면 된다.
입장과 퇴장이라고 생각을 하자
생성자
는 클래스의 초기화(멤버 변수)를 돕기위해 만들어졌고,
소멸자
는 클래스의 청소(메모리 정리)를 위해 만들어졌다고 볼 수 있다.
여기서 이런 말이 나올 수 있다.
"꼭 호출되는 함수라면서 클래스에
생성자
와소멸자
를 안만들어도 작동이 잘되는데요?"
이렇게 생성자
와 소멸자
를 만들어주지 않는 경우에는 자동을 컴파일러
가 기본 생성자(default constructor)와 소멸자(default destructor)를 만들어 준다.
그리고 소멸자
는 하나만 만들 수 있지만 생성자
같은 경우에는 여러개를 만들어줄 수 있다.
class Knight
{
public:
// 기본 생성자
Knight()
{
_hp = 0;
_attack = 0;
_defence = 0;
}
// 기타 생성자(생성자 오버로딩)
Knight(int hp, int attack, int defence)
{
_hp = hp;
_attack = attack;
_defence = defence;
}
public:
int _hp = 0;
int _attack;
int _defence;
};
만든 생성자를 보통 기타 생성자
라고 부른다.
그리고 만약 _hp라고 네이밍 컨벤션을 안정하고 hp라고 멤버 변수를 만들었을 때
hp = hp
라고 하면 컴파일러가 구분을 못하기 때문에
this->hp = hp
this포인터로 자기자신의 hp를 가르켜서 값을 집어넣을 수 있다.
기타 생성자
를 만들면 knight 객체
를 생성할 때 자신이 원하는 값으로 초기화를 하여 생성할 수 있다.
Knight k1(100, 10, 1);
그리고 컴파일러
는 우리가 다른 생성자
를 만들게 된다면 기본 생성자
를 더 이상 만들어주지 않게 되기 때문에 우리가 만든 생성자
형식으로 생성을 하거나 기본 생성자
형식으로 생성자
를 직접 하나 더 만들어 주면 된다.
그리고 또 복사 생성자
라는 것이 있다.
복사 생성자
는 동일 타입의 객체
를 복사해서 동일한 상태의 데이터를 가지는 다른 객체
를 만드는 것이다.
Knight(const Knight& other)
{
_hp = other.hp;
_attack = other.attack;
_defence = other.defence;
}
이런식으로 복사 생성자
를 만들 수 있다.
그럼 어떻게 사용할까?
Knight k1(100, 10, 1);
Knight k2(k1);
이렇게 사용하면 k2가 k1의 데이터를 복사해서 가져오기 때문에 완전히 동일한 값을 가지는 것을 알 수 있다.
우리는 객체지향 프로그래밍
을 할 때
클래스
라는 설계도로 객체(인스턴스)
를 만들어주고 그 객체
들의 생성 이후에는 각각 독립적으로 관리해줄 수 있다.
그리고 클래스의 멤버 변수
, 함수
는 어떤 특정 객체
에 종속적이다.
라는 점이 중요하다고 볼 수 있다.
그리고 객체지향의 3대 요소(oop 3대 요소)라고 해서
상속성(Inheritance)
은닉성(캡슐화(Encapsulation))(Data Hiding)
다형성(Polymorphism)
등이 있는데 이것들은 다음에 시간이 된다면 블로그에 정리를 해보겠다.