본문 바로가기
개인공부/Rookiss C++ 프로그래밍 입문

Chapter 6 - 객체지향 여행

by 하고싶은건많은놈 2023. 3. 29.

객체지향의 시작

절차(procedural)지향 프로그래밍 : 함수가 메인
객체(object)지향 프로그래밍 : 객체가 메인, 객체에는 데이터와 데이터를 조작하는 동작이 모두 포함됨

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;
}

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();

    return 0;
}

class는 일종의 설계도 역할을 함
객체의 크기는 멤버 변수 데이터 크기의 합이 됨


생성자와 소멸자 #1

생성자(constructor) : 클래스의 시작을 알리는 함수, 여러개 존재 가능
소멸자(destructor) : 클래스의 끝을 알리는 함수, 하나만 존재

class Knight
{
    public:
        Knight() { cout << "기본 생성자 호출" << endl; };
        Knight(const Knight& knight)
        {
            _hp = knight._hp;
            _attack = knight._attack;
            _posX = knight._posX;
            _posY = knight._posY;
        }
        Knight(int hp)
        {
            _hp = hp;
            _attack = 10;
            _posX = 0;
            _posY = 0;
        }
        ~Knight() { cout << "소멸자 호출" << endl; };
        ...
}

Knight() : 기본 생성자, 인자 없음

  • 생성자를 명시하지 않은 경우, 기본 생성자가 컴파일러에 의해 자동으로 만들어져 암시적(implicit) 생성자로 사용됨
  • 생성자를 명시적(explicit)으로 선언한 경우 기본 생성자는 생성되지 않음
  • 기본 생성자 외에도 함수의 오버로딩을 사용하여 필요에 따라 여러 종류의 생성자를 설정할 수 있음

Knight(const Knight& knight) : 복사 생성자, 동일한 타입의 다른 객체를 이용하여 초기화할 때 사용

  • 복사 생성자를 명시하지 않은 경우 암시적 복사 생성자가 사용됨
  • 암시적 복사 생성자는 객체의 모든 멤버를 동일하게 복사함

~Knight() : 소멸자


생성자와 소멸자 #2

Knight k2(k1); Kngiht k3 = k1;은 복사 생성자가 사용됨
Knight k4; k4 = k1;k4를 생성자를 사용하여 생성한 후 k1을 대입함

Knight(int hp)와 같이 인자를 1개만 받는 생성자를 타입 변환 생성자라고 부르기도 함

  • 타입 변환 생성자가 존재하는 경우 암시적 형변환이 문제가 될 수 있음
  • 따라서 명시적인 용도로만 사용하도록 생성자 앞에 explicit 키워드를 붙여 활용할 수 있음

상속성

클래스는 상속성을 가지기 때문에 유용하게 재사용할 수 있음

class Player
{
    public:
        Player()
        {
            _hp = 0;
            _attack = 0;
            _defence = 0;
            cout << "Player 기본 생성자 호출" << 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 Knight : public Player
{
    public:
        Knight()
        // 선처리 영역, 여기서 부모의 생성자를 호출
        {
            _stamina = 100;
             cout << "Knight 기본 생성자 호출" << endl; 
        }
        Knight(int stamina) : Player(100)
        {
            _stamina = stamina;
            cout << "Knight(int stamina) 생성자 호출" << endl;
        }
        ~Knight() { cout << "Knight 소멸자 호출" << endl; }
        int _stamina;
}

class Mage : public Player
{
    public:
        int _mp;
}

int main()
{
    Knight k;
    k._hp = 10;
    k.Attack();
}

부모 클래스에 정의되어있는 함수를 재정의할 수 있음
자식 클래스에도 생성자가 있어야하며, 부모의 생성자 먼저 호출 후 자식의 생성자가 호출됨

  • 정확히는 자식 클래스 생성자의 선처리 영역에서 부모의 생성자가 먼저 호출된 후 자식 클래스 생성자의 구문들을 처리함
  • 소멸자는 생성자의 역순으로 호출됨

Kngiht(int stamina) : Player(100)과 같이 사용하여 부모의 생성자를 선택할 수 있음


은닉성

은닉성(Data Hiding) : 클래스에서는 조작해서는 안되는 데이터를 은닉할 수 있음

class Car
{
    public:
        void MoveHandle() {}
        void PushPedal() {}
        void OpenDoor() {}
        void TurnKey()
        {
            ...
            RunEngine();
        }
    protected:
        void RunEngine() {}
    private:
        void DisassembleCar() {}
        void ConnectCircuit() {}
    public:
        // 핸들
        // 페달
        ...
};

class SuperCar : public Car // 여기서의 public은 상속 접근 지정자로 쓰임
{
    public:
        void PushRemoteCOntroller()
        {
            RunEngine();
        }
}

멤버 접근 지정자(제한자)

  • public : 누구든지 접근 가능
  • protected : 파생 클래스에서는 자유롭게 접근 가능
  • private : 해당 클래스 내부에서만 접근 가능

상속 접근 지정자를 통해 현재의 부모를 다음 세대에 어떻게 전달할 것인지 선택할 수 있음

  • public : 부모의 멤버 지정을 그대로 사용
  • protected : 부모의 publicprotected로 전달
  • private : 모든 멤버를 private로 전달
  • 대부분 public으로 사용됨
  • 생략할시에는 private로 적용됨
class Berserker
{
    public:
        int GetHP() { return _hp; }
        void SetHP(int hp)
        {
            _hp = hp;
            if (_hp < 50)
                SetBerserkerMode();
        }
    private:
        int _hp = 100;
        void SetBerserkerMode()
        {
            cout << "매우 강해짐!" << endl;
        }
}

int main()
{
    Berserker b;
    b.SetHP(20);
}

캡슐화(Encapsulation) : 연관된 데이터와 함수를 논리적으로 묶어놓은 것
데이터의 조작 경로를 제어하는데 매우 유용하게 사용됨


다형성 #1

다형성(Polymorphism) : 겉은 똑같아도 기능이 다르게 동작하는 것

  • 오버로딩(Overloading) : 함수 중복 정의 = 함수 이름의 재사용
  • 오버라이딩(Overriding) : 부모 클래스의 함수를 자식 클래스에서 재정의
class Player
{
    public:
        void Move() { cout << "Move Player" << endl; }
        void Move(int a) { cout << "Move Player(int)" << endl; }
    public:
        int _hp;
};

서로 다른 시그니처를 가지고있는 Move()는 오버로딩으로 활용되어 에러를 발생시키지 않음

class Knight : public Plyaer
{
    public:
        void Move() { cout << "Move Knight" << endl; }
    public:
        int _stamina;
};

class Mage : public Player
{
    public:
        int _mp;
};

void MovePlayer(Player* player)
{
    player->Move();
}

void MoveKnight(Kngiht* knight)
{
    knight->Move();
}

int main()
{
    player p;
    MovePlayer(&p);
    MovePlayer(&p);  // 에러 - 플레이어는 기사가 아닐 수 있음

    Knight k;
    MoveKnight(&k);
    MovePlayer(&k);  // 정상작동 - 기사는 반드시 플레이어임

    return 0;
}

MovePlayer() 함수는 모든 자식 클래스들이 사용할 수 있음
따라서 MoveKnight() 함수와 같이 각각의 자식 클래스별로 함수를 정의할 필요가 없기 때문에 훨씬 효율적인 구조를 작성할 수 있음
단, 이 경우 컴파일 시점에 어떤 함수를 실행할지가 결정되는 정적 바인딩이 되어있기 때문에 Move()는 오버라이딩된 함수 대신 부모 클래스의 함수를 호출함

class Player
{
    public:
        ...
        virtual void VMove() { cout << "Move Player" << endl; }
    ...
}

class Knight : public Plyaer
{
    public:
        virtual void VMove() { cout << "Move Knight" << endl; }
    ...
};

void MovePlayer(Player* player)
{
    player->Move();
}

int main()
{
    Knight k;
    MovePlayer(&k); 
}

오버라이딩된 함수를 사용하기 위해서는 virtual 키워드를 통해 가상 함수를 만들어 동적 바인딩을 적용해야함
동적 바인딩은 실행 시점에 어떤 함수를 사용할지가 결정됨
부모 클래스에서 가상 함수로 선언했을 경우, 자식 클래스에서 재정의를 하여도 항상 가상 함수로 취급됨


다형성 #2

가상 함수를 사용하면 생성자의 선처리 영역에서 가상 함수 테이블이 생성됨
가상 함수 테이블에는 가상으로 선언된 함수들의 주소가 저장됨
가상 함수를 사용시에는 해당 객체가 가지고있는 가상 함수 테이블을 참조하여 어느 함수를 선택할 것인지가 결정됨

순수 가상 함수 : 구현 없이 인터페이스만 전달하는 용도로 사용하고 싶을 경우에 사용

  • virtual void VAttack() = 0;의 형식으로 사용
  • 이 경우 부모에서는 함수가 정의되지 않으며, 반드시 상속받는 자식 클래스에서 해당 함수를 정의하여 사용하여야 함
  • 순수 가상 함수가 선언된 순간 해당 함수를 포함한 클래스는 추상 클래스로 간주됨
  • 추상 클래스의 객체는 선언될 수 없음

초기화 리스트

초기화를 하지 않으면 각 데이터에 쓰레기 값이 들어가서 문제가 발생할 수 있음

class Player
{
    public:
        Player() {}
        Player(int id) {}
}

class Inventory
{
    public:
        Inventory() { cout << "Inventory()" << endl; }
        Inventory(int size) { cout << "Inventory(int)" << endl; _size = size; }
        ~Inventory() { cout << "~Inventory()" << endl; }
    public:
        int _size = 10;
}

class Knight : public Player
{
    public:
        Knight() : Player(1), _hp(100), _inventory(20) // 초기화 리스트 
        {
            _hp = 100 // 생성자 내에서 초기화
        }
    public:
        int _hp = 100; // C++11 문법
        Inventory _inventory;
}

C+11 문법을 이용한 초기화 : 클래스 내에서 멤버 변수를 선언함과 동시에 초기화
초기화 리스트를 이용한 초기화 : 상속 관계에서 원하는 부모 생성자 호출에 사용
생성자 내에서 초기화 : 생성자 내에서 평범하게 초기화

KnightPlayer같이 Is-A 관계인 경우 상속관계
KnightInventory같이 has_A 관계인 경우 포함관계

포함관계의 경우에도 포함되는 클래스의 생성자가 선처리 영역에서 암시적으로 생성됨
즉, 생성자 내에서 포함되는 클래스의 생성자를 호출할 경우 앞서 기본 생성자로 생성된 클래스 객체가 소멸됨
반면 초기화 리스트를 사용하여 포함되는 클래스를 초기화할 경우에는 중복 문제가 발생하지 않음

참조나 const타입과 같이 정의와 동시에 초기화가 필요한 경우에도 초기화 리스트를 사용해야함


연산자 오버로딩 #1

class Position
{
    public:
        Position operator+(const Position& arg)
        {
            Position pos;
            pos._x = _x + arg._x;
            pos._y = _y + arg._y;
            return pos;
        }

    public:
        int _x;
        int _y;
};

Position operator+(int arg, const Position& src)
{
    Position pos;
    pos._x = src._x + arg;
    pos._y = src._y + arg;
    return pos;
}

int main()
{
    Position pos;
    pos._x = 0;
    pos._y = 0;

    Position pos2;
    pos2._x = 1;
    pos2._y = 1;

    Position pos3 = pos + pos2;
    Position pos4 = 1 + pos3;
}

연산자는 피연산자의 개수/타입이 고정되어있음
연산자 오버로딩을 이용하여 연산자가 사용자가 정의한 타입의 피연산자를 처리하도록 만들 수 있음

  • 연산자 오버로딩을 위해선 연산자 함수를 정의해야함
  • 연산자 함수는 멤버 함수와 전역 함수 두가지 방법으로 만들 수 있음
    • 멤버 함수로 사용시 a op b 형태로 사용, 왼쪽을 기준으로 실행되며 a는 기준 피연산자로 반드시 클래스여야 함
    • 전역 함수로 사용시 a op b에서 ab 모두를 피연산자로 간주함
    • 단, 대입 연산자같은 경우 전역 함수로 사용할 수 없음

연산자 오버로딩 #2

Position& operator=(int arg)
{
    _x = arg;
    _y = arg;
    return *this;
}

위와 같은 형식으로 객체가 자기 자신을 반환하도록 대입 연산자를 오버로딩할 수 있음

Position& operator=(Position& arg)
{
    _x = arg._x;
    _y = arg._y;
    return *this;
}

복사 대입 연산자란 위와같이 대입 연산자 중 자기 자신의 참조 타입을 인자로 받는 것을 뜻함
객체가 복사되길 원하는 특성 때문에 사용됨

연산자 중에는 ::, ., .* 등 오버로딩할 수 없는 것들도 존재함
++, --는 전위형(++a)의 경우 operator++(), 후위형(a++)의 경우 operator++(int)로 사용함

  • 전위형은 중첩하여 사용할 수 있으나 후위형은 중첩하여 사용할 수 없음
  • 따라서 전위형은 자기 자신을 리턴하도록 하는 것이 유용함

객체지향 마무리

C++에서 structclass는 거의 차이가 없음
struct는 기본 접근 지정자가 public, classprivate
일반적으로 struct는 구조체(데이터 묶음)을 표현하는 용도로만 사용
class는 객체 지향 프로그래밍의 특징을 나타내는 용도로 사용

class Marine
{
    public:
        int _hp;
        int _att;
}
int main()
{
    Marine m1;
    m1._hp = 40;
    m1._att = 6;

    Marine m2;
    m2._hp = 40;
    m2._att = 6;

    m1._att = 7;
    m2._att = 7;
}

위와 같은 경우 마린의 공격력 업그레이드를 한 경우 모든 마린 객체를 찾아 공격력을 올려주어야 함

class Marine
{
    public:
        int _hp;
        static int s_att;
        static void Setattack()
        {
            s_att += 1;
        }
};

int Marine::s_att = 0;

int main()
{
    Marine::s_attack = 6;

    Marine m1;
    m1._hp = 35;

    Marine m2;
    m2._hp = 14;

    Marine::Setattack();
}

이런 불편함을 해소하기 위해 위와 같이 static 변수를 사용할 수 있음
static으로 선언된 변수는 특정 개체의 변화에 무관하며, 클래스 자체와 연관되어있음
함수도 static으로 지정하는 것이 가능하지만, static 함수 내에서는 static 변수만 조작할 수 있음
static 변수는 데이터 영역에 저장됨

  • 프로그램의 시작시 메모리에 올려지며, 종료시 해제됨
  • 단, 해당 변수가 선언된 함수 내에서만 사용할 수 있음

'개인공부 > Rookiss C++ 프로그래밍 입문' 카테고리의 다른 글

Chatper 8 - 실습  (0) 2023.03.29
Chapter 7 - 동적 할당  (0) 2023.03.29
Chapter 5 - 포인터  (0) 2023.03.29
Chapter 4 - 함수  (0) 2023.03.29
Chapter 3 - 코드의 흐름 제어  (0) 2023.03.29

댓글