C++

object

레나19 2022. 3. 27. 13:22

객체지향 시작

#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

'C++' 카테고리의 다른 글

C++ 알고리즘 서적 정리  (0) 2022.04.09
동적할당  (0) 2022.04.01
  (0) 2022.03.20
포인터  (0) 2022.03.14
함수  (0) 2022.03.10