객체지향 시작
#include <iostream>
using namespace std;
//오늘의 주제 : 객체지향 프로그래밍
// 데이터 + 가공(로직, 동작)
// 객체지향 = 객체
// 객체란? 플레이어, 몬스터, GameRoom
// Knight를 설계해보자.
// - 속성(데이터) : hp, attack, y, x
// - 기능(동작) : Move, Attack, Die
// 클래스와 struct의 차이점은 은닉성 차이밖에 없다.
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;
};
void Knight::Move(int y, int x)
{
posY = y;
posX = x;
cout << "Move" << endl;
}
void Knight::Attack()
{
cout << "Attack : " << attack<<endl;
}
//전역변수
void Move(Knight* knight, int y, int x)
{
knight->posY = y;
knight->posX = x;
}
// 객체를 만든다.
int main()
{
Knight k1;
k1.hp = 100;
k1.attack = 10;
k1.posY = 0;
k1.posX = 0;
Knight k2;
k2.hp = 80;
k2.attack = 5;
k2.posY = 1;
k2.posX = 1;
k1.Move(2, 2);
k1.Attack();
k1.Die();
Move(&k1, 3, 3); // 여태까지는 이런 식으로 해왔다. 하지만 객체를 만들면 위처럼 편해진다.
return 0;
}
생성자와 소멸자
#include <iostream>
using namespace std;
//오늘의 주제 : 객체지향 프로그래밍, 생성자와 소멸자
// 데이터 + 가공(로직, 동작)
// 객체지향 = 객체
// 객체란? 플레이어, 몬스터, GameRoom
// Knight를 설계해보자.
// - 속성(데이터) : hp, attack, y, x
// - 기능(동작) : Move, Attack, Die
// 클래스와 struct의 차이점은 은닉성 차이밖에 없다.
// 설계도를 만들었다고 당장 메모리에 올라가는 건 아니다.
// [생성자(Constructor)와 소멸자(Destructor)]
// 클래스에 '소속'된 함수들을 멤버 함수라고 함
// 이 중에서 굉장히 특별한 함수 2종이 있는데 바로 [시작]과 [끝]을 알리는 함수들
// - 시작(탄생) -> 생성자 (여러개 존재 가능)
// - 끝(소멸) -> 소멸자 (오직 1개만)
// [암시적(implicit) 생성자
// 생성자를 명시적으로 만들지 않으면,
// 아무 인자도 받지 않는 [기본 생성자]가 컴파일러에 의해 자동으로 만들어짐.
// -> 그러나 우리가 명시적(Explicit)으로 아무 생성자 하나 만들면,
// 자동으로 만들어지던 [기본 생성자] 는 더 이상 만들어지지 않음!
class Knight
{
public:
// [1] 기본 생성자 (인자가 없음)
Knight()
{
cout << "Knight() 기본 생성자 호출" << endl;
_hp = 100;
_attack = 10;
_posX = 0;
_posY = 0;
}
// [2] 복사 생성자 (자기 자신의 클래스 참조 타입을 인자로 받음)
// 일반적으로 '똑같은' 데이터를 지닌 객체가 생성되기를 기대한다.
//그런데 이거 복사생성자 적어놓지 않아도, 디폴트로 만들어져있다.
// 그래서 이거 지우고 같은 방식으로 main에 적어서 사용하면 실행이 된다.
Knight(const Knight& knight)
{
_hp = knight._hp;
_attack = knight._attack;
_posX = knight._posX;
_posY = knight._posY;
}
// [3] 기타생성자
// explicit을 붙이면 암시적으로 컴파일러가 작동하지 못하게 한다.
// 명시적으로만 작동한다.
explicit Knight(int hp)
{
cout << "Knight() 기본 생성자 호출" << endl;
_hp = hp;
_attack = 10;
_posX = 0;
_posY = 0;
}
Knight(int hp, int attack, int posX, int posY)
{
_hp = hp;
_attack = attack;
_posX = posX;
_posY = posY;
}
// 기타 생성자중 매개변수 1개만 받는 기타생성자를
// 타입 변환 생성자 라고 부르기도 한다.
//
// 기타 생성자 하나라도 만들면 암시적 기본생성자는 더 이상 작동하지 않는다.
// explicit : 명시적인 용도로만 사용할 것 이라는 의미로 만드는 생성자
// 이렇게 설정을 해두면, 암시적으로는 사용이 안된다.
// 예를 들어 알아서 컴파일러가 이 생성자를 사용해주는 일이 없게 만든다.
// Ex) main에서 Knight k1 = 3; or
// Knight k1; k1 = 3; 이렇게 입력하면 컴파일러가 알아서 Knight(3) 이렇게 해줘서
// hp에 3이 들어가게 된다. 이런 식으로 하면 헷갈리게 됨.
// 이런 암시적 사용을 방지하기 위해 explicit을 넣어주면 Knight k1(3); 이렇게만 사용할 수 있다.
// 소멸자
~Knight()
{
cout << "~Knight() 기본 소멸자 호출" << endl;
}
//멤버 함수 선언
// 같은 클래스 내에서는 멤버변수에 멤버함수 접근할 수 있다.
void Move(int y, int x);
void Attack();
void Die()
{
this; // 디어셈블리어로 디버깅해보면 this; 가 생략되어있는 것을 알 수 있다. this는 자기자신의 주소.
_hp = 0;
//this->_hp = 1; //위에꺼에 this가 생략되어있다.
cout << "Die" << endl;
}
public:
// 멤버 변수
// 멤버 변수 표기 방법 : m_hp; mHp; _hp;
int _hp;
int _attack;
int _posY;
int _posX;
};
void Knight::Move(int y, int x)
{
_posY = y;
_posX = x;
cout << "Move" << endl;
}
void Knight::Attack()
{
cout << "Attack : " << _attack<<endl;
}
//전역변수
void Move(Knight* knight, int y, int x)
{
knight->_posY = y;
knight->_posX = x;
}
void HelloKnight(Knight k)
{
cout << "Hello Knight" << endl;
}
// 객체를 만든다.
int main()
{
Knight k1;
k1._hp = 100;
k1._attack = 10;
k1._posY = 0;
k1._posX = 0;
Knight k2(k1); // 복사 생성자의 활용. , 위 객체에 복사생성자 만들어놓지 않아도 자동으로
//복사 생성자 이용하는 줄 알고 컴파일 된다.
Knight k3 = k1; // 이렇게 해도 위와 같다. 복사생성자와 같은 메커니즘으로 돌아감.
//클래스를 만들면 자동으로 기본생성자, 복사생성자가 호출이 된다.
Knight k4; // 기본 생성자 호출
k4 = k1; // 그 다음 복사가 일어난다. 복사생성자와 작동이 다르다.
Knight k5(100, 20, 3, 3); // 기타생성자 호출
k1.Move(2, 2);
k1.Attack();
k1.Die();
//암시적 형변환 -> 컴파일러가 알아서 바꿔치기
int num = 1;
float f = (float)num; // 명시적 << 우리가 코드를 num을 float 바구니에 넣으라고 주문하고 있음
double d = num; // 암시적 << 별 말 안했는데 컴파일러가 알아서 처리하고 있음.
Knight k6;
// HelloKnight(5); // explicit을 안해주면 이게 아무런 문제 없이 컴파일 에러가 발생하지 않는다.
//위에 explicit을 해주었기 때문에 문제 발생하는 거다. Knight k = 5; 로 입력이 되기 때문이다.
// HelloKnight((Knight)5); //이거는 에러나지 않는다.
Move(&k1, 3, 3); // 여태까지는 이런 식으로 해왔다. 하지만 객체를 만들면 위처럼 편해진다.
return 0;
}
생성자의 세 가지 - 기본 생성자, 복사 생성자, 기타 생성자. 기타 생성자 중 매개변수 1개만 받는 생성자를 단일 타입 생성자라고 하며, 암시적 형변환이나 암시적으로 컴파일되어 잘못 사용될 가능성이 있기에 대체로 explicit 선언을 해주어 명시적으로만 사용하게 해준다.
상속성
#include <iostream>
using namespace std;
// 오늘의 주제 : 상속성
// 객체지향 (OOP Object Oriented Programming)
// - 상속성
// - 은닉성
// - 다형성
// class는 일종의 설계도
struct StatInfo
{
int hp;
int attack;
int defence;
};
// 상속 (Inheritance) ? 부모 -> 자식에게 유산을 물려주는 것.
// 생성자(여러 개 생성 가능)/ 소멸자 (1개)
// Player 상속한 Knight는 어느 클래스의 생성자를 호출할까? Player? or Knight?
// 둘 다 호출하자!
// GameObject
// - Creature
// -- Player, Monster, Npc, Pet
// - Projectile
// -- Arrow, Fireball
// -Env
// Item
// - Weapon
// -- Sword, Bow
// - Armor
// -- Helmet, Boots, Armor
// - COnsumable
// -- Potion, Scroll
// 메모리
// 2층 [ Knight ]
// 1층 [ [ Player ] ]
//Player를 기반으로 Knight 건축을 하는 그런 느낌
// 생성자도 소멸자도 건축이랑 똑같다고 보면 된다.
// Player 먼저 짓고, 그 다음 Knight 올리고,
// 건물 해체할때도 Knight 먼저 부수고, 그 다음 Player 부수고.
class Player
{
public:
Player()
{
_hp = 0;
_attack = 0;
_defence = 0;
cout << "Player() 기본 생성자 호출" << endl;
}
Player(int hp)
{
_hp = hp;
_attack = 0;
_defence = 0;
cout << "Player(int hp) 기본 생성자 호출" << endl;
}
~Player()
{
cout << "~Player() 소멸자 호출" << endl;
}
void Move() { cout << "Player Move 호출" << endl; }
void Attack() { cout << "Player Attack 호출" << endl; }
void Die() { cout << "Player Die 호출" << endl; }
public:
int _hp;
int _attack;
int _defence;
};
class Knight111
{
public:
void Move() {}
void Attack() {}
void Die() {}
public:
StatInfo _statInfo; // 이렇게 struct 에서 가져와도 되긴 하는데, struct에
// 함수를 못넣기 때문에 .. 그러면 함수는 어떻게 하나?
};
class Mage111
{
public:
void Move() {}
void Attack() {}
void Die() {}
public:
int _hp;
int _attack;
int _defence;
};
class Knight : public Player //위의 Player를 상속하였다.
{
public:
Knight()
{
/*
선(먼저)처리 영역
-여기서 Player() 생성자를 호출(디어셈블리어로 보면)
*/
_stamina = 100;
cout << "Knight() 기본 생성자 호출" << endl;
}
Knight(int stamina) : Player(100)
/*
선(먼저)처리 영역
-여기서 Player(int hp) 생성자를 호출
*/
{
_stamina = stamina;
cout << "Knight(int stamina) 생성자 호출" << endl;
}
~Knight()
{
cout << "~Knight() 소멸자 호출" << endl;
} // 브레이크 포인트를 여기 걸어주면 소멸자 호출 return 0; 다음 호출되는 거 확인가능
/*
후 (나중에) 처리영역
- 여기서 -Player() 소멸자를 호출
*/
// 재정의 : 부모님의 유산을 거부하고 새롭게 구현
void Move() { cout << "Knight Move 호출" << endl; }
public:
int _stamina;
};
class Mage : public Player
{
public:
public:
int _mp;
};
int main()
{
// Knight k;
Knight k(100);
// 생성자 소멸자 순서
// 조상클래스 생성자 먼저 호출, 그 다음 자손 클래스 생성자 호출
// 자손 클래스 소멸자 호출, 그 다음 조상 클래스 소멸자 호출
//디어셈블리로 보면 자손클래스가 먼저 호출되긴 하는데 호출되는 도중에 부모 클래스 생성자 호출되고나서
// 그 다음 빠져나가면서 자손클래스 생성자가 호출된다.
k._hp = 100;
k._attack = 10;
k._defence = 5;
k._stamina = 50;
// k.Attack();
// k.Die(); // 상속을 받았기 때문에 Player에 있는 거 다 사용할 수 있다.
return 0; // 브레이크 포인트를 여기에다가 걸어준다. ~Knight 소멸자가 실행되는 것을 볼 수 있다.
}
은닉성
#include <iostream>
using namespace std;
// 오늘의 주제 : 은닉성
// 객체지향 (OOP Object Oriented Programming)
// - 상속성
// - 은닉성
// - 다형성
// 은닉성 (Data Hiding) = 캡슐화 ( Encapsulation )
// 몰라도 되는 것은 깔끔하게 숨기겠다!
// 숨기는 이유?
// 1) 정말 위험하고 건드리면 안되는 경우
// 2) 다른 경로로 접근하길 원하는 경우
// 자동차
// - 핸들
// - 패달
// - 엔진
// - 문
// - 각종 전기선
// 일반 구매자 입장에서 사용하는 것?
// - 핸들/ 패달/ 문
// 몰라도 되는 부분 (건드리면 큰일나는 부분)
// - 엔진, 각종 전기선
// public(공개적) protected(보호받는) private (개인의)
// - public : 누구한테나 공개, 마음대로 사용하세요.
// - protected : 나의 자손들한테만 허락
// - private : 나만 사용할거. << class Car 내부에서만 사용 가능
//상속 접근 지정자 : 다음 세대한테 부모님의 유산을 어떻게 물려줄지?
// 부모님한테 물려받은 유산을 꼭 나의 자손들한테도 똑같지 물려줘야 하진 않음.
// - public: 공개적 상속 -> 부모님의 유산 그대로~ (public-> public, protected-> protected)
// - protected: 보호받는 상속? 내 자손들한테만 물려줄거야.( public-> protected, protected -> protected)
// 나까지만 잘 쓰고 다음 아이들은 protected로 받는다.
// - private: 개인적인 상속 -> 나까지만 잘 쓰고 자손들한테는 안물려준다.(public-> private, protected-> private)
// 자기자신만 쓰고 자손클래스는 자기꺼를 사용하지 못한다.
// protected와 private은 거의 쓰이지 않는다.
// 상속시 아무것도 앞에 안써놓으면 예를 들어, class TestSuperCar : SuperCar. 이렇게 상속 접근지정자를 안쓰면
// 자동으로 private으로 디폴트설정된다.
class Car
{
public: // (멤버) 접근 지정자
void MoveHandle() { }
void PushPedal() { }
void OpenDoor() { }
void TurnKey()
{
// ...
RunEngine();
}
//사용자가 만지면 안되는 부분. 숨기고 싶은 부분.
protected: //private 으로 설정하면 밑에 main에서 실행안된다.
void DisassembleCar() { } // 차를 분해한다.
void RunEngine() { } // 엔진을 구동시킨다
void ConnectCircuit() { } // 전기선 연결
public:
// 핸들
// 패달
// 엔진
// 문
// 각종 전기선
};
class SuperCar : public Car // 상속 접근 지정자
{
// Car에서 private으로 지정된 거는 SuperCar에서 사용할 수 없다.
// Car에서 protected로 지정된 거는 SuperCar에서 사용할 수 있다.
public:
void PushRemoteController()
{
RunEngine();
}
};
// '캡슐화'
// 연관된 데이터와 함수를 논리적으로 묶어놓은 것
class Berserker
{
public:
int GetHp() { return _hp; }
void SetHp(int hp)
{
_hp = hp;
if (hp <= 50)
SetBerserkerMode();
}
// 사양 : 체력이 50 이하로 떨어지면 버서커 모드 발동 (강해짐)
private:
void SetBerserkerMode()
{
cout << "매우 강해짐!" << endl;
}
private:
int _hp = 100;
};
int main()
{
Car car;
SuperCar supercar;
supercar.PushRemoteController(); //RunEngine은 PushRemoteController를 통해서 이용가능.
Berserker b;
//b._hp = 20; // 이렇게 건드리면 이 밑에 버서커모드를 다시 적어놔야 한다. 하지만
//if(b._hp<50) b.SetBerserkerMode(); 이런 식으로 여기다 적어줘야한다.
//public으로 _hp를 해놓고, 누가 건드리면 안되니까. 캡슐화로 private 으로 해놓고
// getset으로 원하는 숫자의 유효성을 설정해놓은다음 출력해준다.
b.SetHp(20); // 이렇게만 해도 버서커모드 표현 가능.
return 0;
}

다형성
#include <iostream>
using namespace std;
// 오늘의 주제 : 다형성
// 객체지향 (OOP Object Oriented Programming)
// - 상속성
// - 은닉성 = 캡슐화
// - 다형성
// 다형성(Polymorphism = Poly + morph) = 겉은 똑같은데, 기능이 다르게 동작한다.
// - 오버로딩(Overloading) = 함수 중복 정의 = 함수 이름의 재사용
// - 오버라이딩(Overriding) = 재정의 = 부모 클래스의 함수를 자식 클래스에서 재정의
// 바인딩(Binding) = 묶는다.
// - 정적 바인딩(Static Binding) : 컴파일 시점에 결정
// - 동적 바인딩(Dynamic Binding) : 실행 시점에 결정
// 일반 함수는 정적 바인딩을 사용한다.
// 동적 바인딩을 원한다면? -> 가상 함수 (virtual function)
// 그런데 실제 객체가 어떤 타입인지 어떻게 알아서 가상함수를 호출해준걸까?
// - 가상 함수 테이블 (vftable)
//. vftable [] 4 바이트 (32비트 환경) 8바이트 (64비트 환경)
// 순수 가상 함수 : 구현은 없고 '인터페이스'만 전달하는 용도로 사용하고 싶은 경우.
class Player
{
public:
void Move() { cout << "Move Player!" << endl; }
void Move(int a) { cout << "Move Player (int)!" << endl; }
virtual void VMove() { cout << "Move Player !" << endl; }
virtual void VDie() { cout << "Die Player !" << endl; }
virtual void VAttack() = 0; // 순수 가상 함수,
// 플레이어를 상속하는 자손클래스가 구현해줘라~ 모던C++ 에서는 abstract를 사용한다.
//순수 가상 함수를 하나라도 만든 순간,
// 추상 클래스 : 순수 가상 함수 1개 이상 포함되면 바로 추상 클래스로 간주
// 직접적으로 객체를 만들 수 없게 됨.
public:
int _hp;
};
class Knight : public Player
{
public:
void Move() { cout << "Move Knight!" << endl; }
virtual void VMove() { cout << "Move Knight !" << endl; }
virtual void VDie() { cout << "Die Knight !" << endl; }
// virtual 쓴 거를 자손클래스에서 재정의해줌.
// 이렇게 하면 다형성에서 Player로 관리를 해줘도 knight에 있는 게 나온다.
// 가상 함수는 재정의를 하더라도 가상 함수다!
// 조상클래스에서 virtual 써서 정의하면 자손클래스는 virtual 안써줘도 가상함수이다.
virtual void VAttack() { cout << "Vattack Knight!" << endl; }
public:
int _stamina;
};
class Mage : public Player
{
public:
int _mp;
};
void MovePlayer(Player* player)
{
// player-> Move(); 아래것과 비교하여 차이를 알아보자. 공부한거니까....
player->VMove();
player->VDie();
}
void MoveKnight(Knight* knight)
{
knight->Move();
}
int main()
{
Player p; // 위에 순수 가상 함수 없애주면 컴파일 에러 없어진다.
MovePlayer(&p); // 플레이어는 플레이어다 ? Yes
// MoveKnight(&p); // 플레이어는 기사다? No.
// 플레이어는 기사일수도 있고, 마법사일수도 있고, 궁수일수도 있고. 그래서 No.
// p.Move(); // 오버로딩 설명
// p.Move(100);
Knight k; // 디버깅해보고 싶으면 생성자 class에 직접 입력해줘야 어떤식으로 이게 생성되는지 더 잘 볼 수 있다.
MoveKnight(&k); // 기사는 기사다? Yes
MovePlayer(&k); // 기사는 플레이어다.? Yes.
// 다형성 이게 가능해짐으로써 어떤 게 가능한가?
// 원래는 MoveKnight(knight) , MoveMage(mage) 이런식으로 각자 만들어야 했는데
// MovePlayer 하나로 모든 자손클래스를 넣어줄 수 있다는게 데이터 관리 면에서 낫다.
// MovePlayer(knight), MovePlayer(mage) 이런 식으로 하면 가독성 면에서 훨씬 낫자나!
// 공통 기능을 묶어서 관리할 수 있다는 엄청난 장점이 있다.
//여기서 문제는
// MovePlayer(&k); 를 호출하면 Move Knight가 아닌 Move Player가 출력이 된다.
// 이거는 왜 그러냐? 이거를 바인딩이라고 하는데 위에 설명해놓았다.
// virtual을 이용해주면 MovePlayer(&k) 를 해줘도 Move Knight! 가 출력되는 것을 확인할 수 있다.
return 0;
}
초기화 리스트
#include <iostream>
using namespace std;
// 오늘의 주제 : 초기화 리스트
// 멤버 변수 초기화? 다양한 문법이 존재
// 초기화 왜 해야할까? 귀찮다.?
// 초기화 안해주면 이상한 숫자가 출력된다.
// 기존사람이 남겨둔 쓰레기 값으로 출력이 된다.
//이래서 들어가자마자 항상 초기화를 해줘야 한다.
// 버그 예방에 중요
// 포인터 등 주소값이 연루되어 있을 경우.
// 초기화 방법
// - 생성자 내에서
// - 초기화 리스트
// - C++ 11 문법
// 초기화 리스트
// - 일단 상속관계에서 원하는 부모 생성자 호출할 때 필요하다.
// - 생성자 내에서 초기화 vs 초기화 리스트
// -- 일반 변수는 별 차이 없음
// -- 멤버 타입이 클래스인 경우 차이가 난다.
// -- 정의함과 동시에 초기화가 필요한 경우 (참조 타입, const 타입)
class Inventory
{
public:
Inventory() { cout << "Inventory()" << endl; }
Inventory(int size) { cout << "Inventory (int size)" << endl; _size = size; }
~Inventory() { cout << "~Inventory()" << endl; }
public:
int _size = 10;
};
class Player
{
public:
Player() { }
Player(int id) { }
};
// Is-A (Knight Is-A Plyaer? 기사는 플레이어다.) Ok-> 상속관계
// Has-A (Knight Has-A Inventory?) Ok 기사는 인벤토리를 가지고 있다. 포함하고 있다.
class Knight : public Player
{
public:
Knight() : Player(10), _hp(100), _inventory(20), _hpRef(_hp),_hpConst(100) // 만약 Player(10) 이런거 안해주면 기본생성자가 출력되고
//Player(10) 이거 해줌으로써 기타생성자 Player(int id)가 출력된다.
// _hp (100) 이런 식으로 초기화 가능하다.
/*
선처리 영역
밑에 Has-A 관계를 만들어줌으로써, Inventory _inventory 해줌으로써
Inventory() 가 Knight의 생성자로 호출이 될거다.
*/
{
_hp = 100;
// _inventory = inventory(20) 이러면 밑에 기본생성자가 만들어지고 다시 새로운 생성자inventory(20) 이렇게
// 만들어져 코드의 낭비가 일어나게 된다. 그것보다 위에 _inventory(20) 이렇게 초기화리스트를 이용해서 적어놓는게 낫다.
//_hpRef = _hp; 이렇게 안된다. &와 const는 바로바로 초기화해주어야 한다.
//_hpConst = 100; 이렇게 안된다.
}
public:
int _hp; // 초기화를 안해주면 쓰레기값이 나온다.
// int _hp = 100; 이렇게 초기화해도 된다. modern C++ 11
Inventory _inventory; // Knight가 Inventory를 들고 있다.
int& _hpRef; // C++ 11 에서 바로 int& _hpRef = _hp;
const int _hpConst; // C++ 11 에서 const int _hpConst = 10; 이렇게 초기화해줄수 있다.
};
// 스택 메모리 : 게속 공유를 하는 개념. 쓰고 지워지고 쓰고 지워지고.
// 초기화 안해주면 지저분하게 그대로 남아있는 상태이다.
// 이로 인해 이상한 숫자가 출력이 된다.
// 기존 사람이 남겨둔 쓰레기값이 출력됨.
int main()
{
Knight k;
cout << k._hp << endl;
return 0;
}
연산자 오버로딩 (이거 너무 어려워서 후반부에 하다가 그만뒀다. 연산자오버로딩2 하다가 그만둠)
#include <iostream>
using namespace std;
// 오늘의 주제 : 연산자 오버로딩(Operator Overloading)
// 연산자 vs 함수
// - 연산자는 피연산자의 개수/ 타입이 고정되어 있음
// 연산자 오버로딩?
// 일단 [연산자 함수]를 정의해야 함
// 함수도 멤버함수 vs 전역함수가 존재하는 것처럼, 연산자 함수도 두가지 방식으로 만들 수 있음.
// - 멤버 연산자 함수 version
// -- a operator b 형태에서 왼쪽으로 기준으로 실행됨 (a가 클래스여야 가능, a를 '기준 피연산자'라고 함)
// -- 한계) a가 클래스가 아니면 사용 못함.
// - 전역 연산자 함수 version
// -- a op b 형태라면 a, b 모두를 연산자 함수의 피연산자로 만들어준다.
// 그럼 무엇이 더 좋은가? 그런거 없다. 심지어 둘 중 하나만 지원하는 경우도 있기 때문이다.
// 상황에 맞게 필요한 거 쓰면 됨.
// - 그리고 대표적으로 대입 연산자 (a=b)는 전역 연산자 version으로 못만든다.
// 복사 대입 연산자
// - 대입 연산자가 나온 김에 [복사 대입 연산자] 에 대해 알아보자.
// 용어가 좀 헷갈린다.[복사 생성자] [대입 연산자] [복사 대입 연산자]
// - 복사 대입 연산자 = 대입 연산자 중, 자기 자신의 참조 타입을 인자로 받는 것.
// 기타
// - 모든 연산자를 다 오버로딩 할 수 있는 것은 아니다.(:: . .* 이런 건 안됨)
// - 모든 연산자가 다 2개 항이 있는 것 아님. ++ -- 가 대표적 (단항 연산자)
// - 증감 연산자 ++ --
// -- 전위형 (++a) 는 operator++() 이렇게 만들어주면 되고
// -- 후위형 (a++) 은 operator++(int) 이렇게 만들어준다.
class Position
{
public:
// RET FUNC_NAME(ARG_LIST)
Position operator+(const Position& arg)
{
Position pos;
pos._x = _x + arg._x;
pos._y = _y + arg._y;
return pos;
}
Position operator+(int arg)
{
Position pos;
pos._x = _x + arg;
pos._y = _y + arg;
return pos;
}
bool operator==(const Position& arg)
{
return _x == arg._x && _y == arg._y;
}
// 대입연산자 안되는 거 예시
Position& operator=(int arg)
{
_x = arg;
_y = arg;
//Position* this = 내 자신의 주소;
return *this;
}
// [복사 생성자] [복사 대입 연산자] 등 특별 대우를 받는 이유는,
// 말 그대로 객체가 '복사'되길 원하는 특징 떄문
Position& operator=(Position& arg)
{
_x = arg._x;
_y = arg._y;
return *this;
}
Position& operator++()
{
_x++;
_y++;
return *this;
}
Position operator++(int)
{
Position ret = *this;
_x++;
_y++;
return ret;
}
public:
int _x;
int _y;
};
//전역함수로 만들어줌.
// a부분이 클래스가 아니어도 더해짐.
Position operator+(int a, const Position& b)
{
Position ret;
ret._x = b._x + a;
ret._y = b._y + a;
return ret;
}
//void operator=(const Position& a, int b)
//{
// a._x = b;
// a._y = b;
//}
// 멤버변수여야 한다는 오류메시지가 뜬다.
// 이런게 가능하면 문법적으로 위험하게 = 에 대해서 정의할 여지가 있기 때문에
// C++ 자체에서 이거를 막아놓았다.
int main()
{
int a = 1;
int b = 2;
int c = a + b;
Position pos;
pos._x = 0;
pos._y = 0;
Position pos2;
pos2._x = 1;
pos2._y = 1;
Position pos3 = pos + pos2;
// pos3 = pos.operator+(pos2); 위의 식과 같은거다.
Position pos4 = pos3 + 1;
Position pos5 = 1 + pos3; //전역함수 만들어준 덕분에 이게 가능해짐.
bool isSame = (pos3 == pos4);
//Position pos7 = 5; 이거 안된다. pos7을 5로 초기화시킨다는 걸로 이해한다.
//이거는 된다.
Position pos6;
pos6 = 5;
Position pos7 = (pos5 = 5);
pos5 = pos3++;
return 0;
}
객체지향의 마무리
#include <iostream>
using namespace std;
// 오늘의 주제 : 객체지향 마무리
// 1) struct vs class
// C++ 에서는 struct나 class나 종이 한장 차이다.
// struct는 기본 접근 지정자가 public이고, class는 private이다.
// 왜 이렇게 했을까? C++는 C언어에서 파생되어 발전했기 때문에.. struct를 안없앰.
// -> struct는 구조체 (데이터 묶음)을 표현하는 용도
// -> class는 객체 지향 프로그래밍의 특징 나타내는 용도
struct TestStruct
{
int _a;
int _b;
};
class TestClass
{
int _a;
int _b;
};
//2) static 변수, static 함수 (static = 정적 = 고정됨)
class Marine
{
public:
// 특정 마린 객체에 종속적
int _hp;
// int _attack; static 사용 전
// static 사용하면 클래스 안에 있지만 실제로는 전역으로 빠져있다. 단지 자리만 class안에 있을 뿐.
// 특정 마린 객체와 무관
// 마린이라는 '클래스' 자체와 연관
// 전역함수에 넣은것마냥 행동함.
static int s_attack;
//멤버 함수 - > 특정 마린 객체에 종속적인 함수
void TakeDamage(int damage)
{
_hp -= damage;
}
//
static void SetAttack()
{
//같은 static끼리만 건드릴 수 있다.
s_attack = 100;
}
};
// static 변수는 어떤 메모리?
// 초기화하면 .data
// 초기화 안하면 .bss 에 올라감
int Marine::s_attack = 0;
class Player
{
public:
int _id;
};
int GenerateId()
{
//정적 지역 객체
//생명주기 : 프로그램 시작/종료 (메모리에 항상 올라가 있음)
//가시범위 : 함수 내부
static int s_id = 1;
return s_id++;
}
int main()
{
TestStruct ts;
ts._a = 1;
TestClass tc;
// tc._a = 1; // 디폴트가 private이기 때문에 public 설정을 해줘야한다.
Marine m1;
m1._hp = 40;
m1.TakeDamage(10);
//m1.s_attack = 6;
Marine::s_attack = 6;
Marine m2;
m2._hp = 40;
m2.TakeDamage(5);
//m2.s_attack = 6;
// 마린 공격력 업그레이드 완료! (Academy에서 업그레이드 끝)
//m1.s_attack = 7;
//m2.s_attack = 7;
Marine::s_attack = 7;
//Marine 안에 있는 SetAttack() 을 설정해주겠다.
Marine::SetAttack();
// 이렇게 객체 하나하나 지정해서 마린 공격력을 올려주는 것보다
// class에서 static을 활용하면 한번에 올려줄 수 있다.
static int id = 10;
// 스택 메모리에 올라가는 것이 아닌 .data 영역에 올라가 있다.
int a = id;
cout << GenerateId() << endl;
cout << GenerateId() << endl;
cout << GenerateId() << endl;
cout << GenerateId() << endl;
cout << GenerateId() << endl;
return 0;
}
1. struct vs class
2. static