[Rookiss C++] 클래스

황교선·2023년 3월 27일
0

cpp

목록 보기
15/19

절차지향 프로그래밍으로 textRPG 코드를 예시로 짜보면 다음과 같다

  • main
    • EnterLobby(PlayerInfo)
      • CreatePlayer
      • EnterGame(MonsterInfo)
        • CreateMonsters
        • EnterBattle

이렇게 프로그램을 짜게 된다면 순서를 이 상태로 설계를 했기 때문에 순서를 바꾸기 힘들어 유연성이 사라지며 코드의 확장이 어렵다. 그렇기 때문에 객체지향언어로 확장성에 용이하게 프로그래밍할 수 있다. C++은 절차지향언어인 C언어에 객체지향언어의 개념인 클래스가 추가되었기 때문에 애매한 포지션에 있지만 그래도 객체지향언어이다. 일단 객체지향언어의 몇 가지 개념에 대해 알아보고 가자.

객체지향언어

객체지향언어의 특징

  • 추상화, Abstraction
    • 불필요한 세부 사항들은 제거하고 가장 본질적이고 공통적인 부분만을 추출하여 표현하는 것
    • 객체의 공통적인 속성과 기능을 추출하여 정의하는 것
    • 역할과 구현의 분리하는데 사용
  • 상속, Inheritance
    • 기존의 클래스를 재활용하여 새로운 클래스를 작성하는 것
    • 상위 클래스가 공유하는 속성과 기능에 접근할 수 있어서 반복적인 코드를 최소화함
    • 구체적인 내용을 정의해두고 하위 클래스가 사용하도록 함
      • 추상화와 상반되는 특징
  • 다형성, Polymorphism
    • 多形性 : 어떤 객체의 속성이나 기능이 상황에 따라 여러 가지 형태를 가질 수 있는 성질
    • 어떤 객체의 속성이나 기능이 그 맥락에 따라 다른 역할을 수행할 수 있는 것
    • 다형성을 활용하기 위해서 추상화와 상속의 개념이 필요
    • 상위 클래스 타입의 변수로 하위 클래스 객체를 참조하는 것
      • 여러 다양한 하위 클래스 객체를 공통된 상위 클래스 타입의 변수로 사용이 가능
      • 다형성을 이용하기에 다양한 하위 클래스의 각기 다른 동작을 신경쓰지 않아도 됨
  • 캡슐화, Encapsulation
    • 서로 연관있는 속성과 기능들을 하나의 캡슐로 만들어 데이터를 외부로부터 보호하는 것
    • 데이터 보호 : 외부로부터 클래스에 정의된 속성과 기능들을 보호
    • 데이터 은닉 : 내부의 동작을 감추고 외부에는 필요한 부분만 노출
    • 객체 내부의 동작의 외부로의 노출을 최소화하여 객체의 자율성을 높이고, 객체 간 결합도를 낮춤

객체지향언어의 특징을 적용하면 역할과 구현을 구분하며 내부 동작을 외부로 최소화하여 노출해 객체들 간의 직접적인 결합을 피하고, 상위 클래스와 공유되는 속성과 기능을 통해 반복적인 코드를 최소화하고, 느슨한 관계 설정을 통해 보다 유연하고 변경이 용이한 프로그램 설계가 가능하다.

설계 원칙

설계원칙은 각 5가지 원칙의 앞글자를 따서 SOLID 원칙이라고도 부른다.

  • 단일 책임 원칙, Single Responsibility Principle
    • 모든 클래스는 각각 하나의 책임만 가져야함
  • 개방-폐쇄 원칙, Open Closed Principle
    • 확장에는 열려있고 수정에는 닫혀있어야함
    • 기존의 코드는 변경하지 않으면서(Closed), 기능을 추가(Open)할 수 있도록 설계가 되어야함
  • 리스코프 치환 원칙, Liskov Substitution Principle
    • 자식 클래스는 언제나 자신의 부모 클래스를 대체할 수 있어야함
    • 자식 클래스는 부모 클래스의 책임을 무시하거나 재정의하지 않으며 확장만 수행
  • 인터페이스 분리 원칙, Interface Segregation Principle
    • 한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 말아야함
    • 하나의 일반적인 인터페이스보다 여러 개의 구체적인 인터페이스가 나음
  • 의존성 역전 원칙, Dependency Inversion Principle
    • 의존 관계를 맺을 때 변화하기 쉬운 것 또는 자주 변화하는 것보다 변화하기 어려운 것에 의존할 것
    • 구체적인 클래스보다 인터페이스나 추상 클래스와 관계를 맺을 것

처음 객체지향언어를 배우면서 이같은 특징과 원칙을 적용하는 것은 쉽지 않으므로 나중에 언어와 엔진에 관해 차차 적응한 후 시도해보면 좋다고 생각한다. 배워야할 것은 한 번에 하나씩, 내가 모르는 개념이나 영역이 한 영역으로만 있어야 공부하기가 수월하니 말이다.

클래스

구조체와 클래스의 차이점

C++ 구조체

  • [핵심] C++은 C와의 호환성을 위해 구조체를 지원함
  • 구조체는 C의 구조체에서 기능을 확장하여 클래스와 동일한 구조와 기능을 가짐
  • 객체 생성 시 struct 키워드를 붙이지 않음

공통점

  • 다른 구조체나 클래스에게 상속이 가능
  • 멤버들은 접근 지정자로 지정되며, 멤버 활용이 동일

차이점

  • 기본 접근 지정자가 다름
    • class : private
    • struct : public

클래스

A user-defined type or data structure declared with keyword class that has data and functions as its members whose access is governed by the three access specifiers private, protected or public

  • 클래스라는 키워드로 정의한 사용자 정의 유형 또는 데이터 구조
  • 데이터와 함수를 멤버로 갖음
    • 데이터 : 멤버변수, 프로퍼티 등으로도 부름
    • 함수 : 멤버함수, 메소드 등으로도 부름
  • 세 가지 접근 지정자를 통해 멤버의 접근이 제어가 됨
    • private, protected, public
// 클래스의 선언
class 클래스이름
{
private: // 접근 지정자, 외부로의 노출을 막고 싶을 때
    멤버변수타입 멤버변수이름1;
protected: // 접근 지정자, 상속 관계에서만 노출되고 싶을 때
    멤버변수타입 멤버변수이름2;
public: // 접근 지정자, 공개하고 싶을 때
    멤버변수타입 멤버변수이름3;

// 접근 지정자는 여러번 사용할 수 있음
// 다음 접근 지정자 선언까지 해당 지정자의 접근 범위로 멤버들이 포함됨
// 가독성이 좋게 멤버 변수와 멤버 함수를 나눠서 사용함
private:
    반환형 멤버함수이름1(매개변수의자료형 매개변수이름1)
    {
        문장들; // return 키워드 포함
    }
protected:
    반환형 멤버함수이름2(매개변수의자료형 매개변수이름2);
public:
    반환형 멤버함수이름3(매개변수의자료형 매개변수이름3);
}; // 클래스 선언의 끝에 세미콜론을 붙여줘야함

반환형 클래스이름::멤버함수이름2(매개변수의자료형 매개변수이름2)
{
    문장들; // return 키워드 포함
}

inline 반환형 클래스이름::멤버함수이름3(매개변수의자료형 매개변수이름3)
{
    문장들; // return 키워드 포함
}
  • 클래스의 구성
    • 클래스의 선언
    • 멤버함수 정의

클래스의 구성은 다음 두 가지로 이루어져있고 클래스의 정의라는 말은 쓰지 않는 것 같다. 멤버함수의 선언은 클래스의 선언 내에 있기도하고 멤버변수는 클래스의 선언에 포함되며 정의할 것이 없에 멤버변수의 정의는 없다.

접근 지정자라는 것은 아래에서 설명하도록하고 그외의 각 다른 멤버 함수의 정의를 보자. 각 다른 방식으로 멤버함수 3개를 정의했는데, 멤버함수이름1은 클래스 내부에 있기 때문에 스코프 연산자(::, 범위 지정 연산자)를 사용하지 않아도 되지만 나머지 두 멤버함수는 클래스 외부에서 정의하고 있기 때문에 어떤 클래스에 속해있는지 알려주기 위해 함수 이름 왼편에 스코프 연산자를 같이 사용한다. 또한 멤버함수이름1과 멤버함수이름3은 인라인함수이다. 멤버함수이름2는 일반 멤버함수이다.

실습 코드

게임 개발을 위해 배우는 C++이니 textRPG 를 구현해가며 객체지향을 실습해보도록 한다. 자동차를 이용하여 배우는 객체지향이 깔끔하고 많이 사용하긴하지만 이 방식을 시도해보도록한다.

코드는 게임이 돌아가게끔 만들면서도 현재 실습해야하는 개념들만 적용할 수 있도록 노력해볼 것이다. 그래서 더 효율적인 방식이 있겠지만 바로 배울 것만 사용하도록한다.

// textRPG Table Size
const int TILE_ROW = 10;
const int TILE_COL = 10;

// textRPG
class Specialty // Class라는 이름을 쓰고 싶지만, 키워드이므로 특기라는 Specialty 사용
{
public: // 접근 지정자
    void Initialize() // 자동 인라인 함수
    {
        m_posX = 0;
        m_posY = 0;
    }
    void Print();
    void Move(int x, int y);
private: // 접근 지정자
    bool IsSafePosition(int x, int y);
protected: // 접근 지정자
    int m_posX;
    int m_posY;
};

void Specialty::Print() // 멤버함수 정의
{
    cout << "Pos : " << m_posX << "," << m_posY << endl;
}

void Specialty::Move(int x, int y) // 멤버함수 정의
{
    if (IsSafePosition(x, y))
    {
        m_posX = x;
        m_posY = y;
    }
}

bool Specialty::IsSafePosition(int x, int y) // 멤버함수 정의
{
    if (x < 0 || y < 0 || TILE_ROW <= x || TILE_COL <= y)
    {
        return false;
    }

    return true;
}

인라인 함수

  • 매크로 함수처럼 함수가 호출될 때 그 위치에 해당 함수가 삽입되도록 함
  • 매개변수나 반환값이 명시되어 있어 매크로 함수보다 정교함
  • 함수를 호출할 때 생기는 지연시간을 없앨 수 있음
  • 호출한만큼 코드에 삽입되기 때문에 프로그램이 커짐
  • 주로 정의가 짧을 때 사용
  • 클래스 밖에서 정의할 때는 inline 이라는 키워드를 붙임
  • 클래스 내에서 정의하는 함수는 자동으로 인라인 함수가 됨

접근 지정자

클래스에서 각 멤버변수나 멤버함수 앞에 붙여 각각에 대한 접근 권한을 지정

public

객체를 사용할 수 있는 범위라면 어디서나 접근 가능한 공개된 멤버

  • 원하는 기능만 공개할 수 있음
  • 일반적으로 private 멤버변수를 처리하기 위해 함수를 public 멤버를 설정함
    • 함수를 통함으로써 멤버변수의 직접 참조를 막을 수 있고 예외를 처리할 수 있음

protected

해당 멤버가 속한 클래스의 상속 관계에서도 사용 가능

  • 상속에 가서 설명하겠지만 현재로써는 private이랑 동일하다 생각하면 됨
  • 자식 관계에게만 기능을 공개할 수 있음

private

해당 멤버가 속한 클래스의 멤버함수에서만 사용 가능

  • 멤버를 은닉할 수 있음
  • 접근 지정자가 생략되면 기본으로 적용됨
  • 일반적으로 멤버변수를 이로 설정함

인스턴스

클래스를 사용하기 위해서는 그 클래스의 타입의 객체를 선언해야하고 이렇게 메모리에 할당된 객체를 말함

클래스를 선언하는 것으로는 메모리 상에 적재되지 않는다. 구조체도 이와 마찬가지다. 우리가 구조체를 정의한다고해서 이 정의된 구조체의 멤버 변수를 바로 다룰 수 없었다. 이 구조체의 변수를 선언해서 선언된 변수의 멤버를 조작했다. 변수를 선언할 때 메모리에 적재되고 우리는 이 메모리에 들어있는 값을 조작하는 것이다. 클래스도 마찬가지다. 정의된 클래스 타입으로 변수를 생성하고 이에 접근해서 사용해야하고, 변수를 생성했을 때 그제서야 메모리에 적재된다.

  • 각 인스턴스는 독립된 메모리 공간에 자신만의 멤버 변수를 가짐
  • 모든 인스턴스는 멤버 함수를 공유함
int main() 
{
    Specialty player1;
    player1.Initialize();
    Specialty player2;
    player2.Initialize();
    player2.Move(2, 3);

    player1.Print();
    player2.Print();

    // player1.IsSafePosition(1,2) // IsSafePosition 함수가 private이기에 클래스 밖인 main에서는 호출 불가능
    // protected도 동일하다고 생각하면 됨
}
// 출력 결과
// Pos : 0,0
// Pos : 2,3

멤버의 접근 방식은 구조체와 동일하다. .와 → 같은 멤버 참조 연산자를 이용한다.

this

컴파일러에 의해 생성되는 포인터
멤버함수를 호출한 객체를 가르키고 멤버함수에서만 사용 가능

  • 멤버함수를 호출한 객체를 가르키며, 호출된 멤버 함수의 숨은 인수로 전달됨
  • 멤버함수 내의 멤버가 누구의 멤버인지 this에 의해 명시됨
  • 멤버변수 앞에 this→를 붙이지 않아도 컴파일러가 암시적으로 붙임
  • 멤버변수와 매개변수가 이름이 같다면 멤버변수에 명시적으로 적어주어야함
void Specialty::Move(int x, int y)
{
    if (IsSafePosition(x, y))
    {
        this->m_posX = x; // this-> 를 명시적으로 붙여준 것
        this->m_posY = y; // this-> 를 명시적으로 붙여준 것
    }
}
// this 포인터를 실체화했을 때의 모습
void Specialty::Move(Specialty* const this, int x, int y) // this 포인터가 숨은 인자로 전달됨, const는 주소를 변경하지 못하게함
{
    if (IsSafePosition(x, y))
    {
        this->m_posX = x;
        this->m_posY = y;
    }
}
player1.Move(&player1, 2, 3) // 호출 예
// 잘못된 방법을 보여주는 예
// 매개변수는 m_을 붙이지 않지만 예시를 보이기 위해 붙임(member가 아니기에 붙이지 않음)
// this->이름과 이름의 차이점을 알기 위해서 멤버변수랑 이름을 같게 만듬
void Specialty::Move(int m_posX, int m_posY) // 매개변수의 이름을 멤버변수와 동일하게 한다면
{
    if (IsSafePosition(m_posX, m_posY))
    {
        m_posX = m_posX; // 멤버변수는 명시적으로 적어주어야 멤버변수에 대입이됨
        m_posY = m_posY; // 적지 않는다면 매개변수에 매개변수값을 대입하는 것이므로 의미가 없고 원하는 기능이 아닌 오류임
    }
}

참고한 글

profile
성장과 성공, 그 사이 어딘가

0개의 댓글