경일게임아카데미 멀티 디바이스 메타버스 플랫폼 개발자 양성과정 20220531 2022/04/04~2022/12/13

Jinho Lee·2022년 5월 31일
0

경일 메타버스 20220531 9주차 2일 수업내용. C++ 프로그래밍 문법

C++ 연습 발표

  1. 생성자와 소멸자는 항상 짝을 지어 놓을 것.
  2. 생성자는 초기화, 멤버 초기화 목록으로 멤버 변수를 모두 초기화할 것.
  3. 클래스 안은 클래스 범위(Class Scope), 모든 멤버 변수는 this포인터를 사용하지 않아도 접근이 가능하다. private이라도!
  4. private는 public의 밑에 써주는 convention이 있다.
  5. 클래스 내부에서의 static의 동작은 아직 배우지 않았다.
  6. 보통 클래스 선언은 헤더 파일에, 함수 구현은 소스 파일에서 이루어진다.
    헤더만을 보여 설명하기 쉽기 때문.
  7. 메소드의 뒤에 const 한정자를 붙이면 메소드의 정보는 변경이 불가하다.
    클래스의 사용에서, 생성의 앞에 const 한정자를 붙이면 const로 선언된 메소드만을 사용할 수 있다.

정리

객체 지향 프로그래밍 개념

  • 절차 지향 프로그래밍이 불편했기 때문에 등장했다.
    • 프로그램 유지보수가 힘들기 때문.
  • 4가지 개념
    • 캡슐화 (Encapsulation) : 데이터와 함수를 같이 작성하는 것.
    • 상속 (Inhaeritance) : 코드를 물려 받는 것.
    • 추상화 (Abstraction) : 사용하기 쉽게 인터페이스를 정의하는 것.
    • 다형성 (Polymorphism) : 같은 인터페이스를 사용하되 서로 다른 동작을 하게 하는 것.
  • 객체에서 데이터를 필드(Field), 함수를 메소드(Method)라고 한다.

C++ 프로그래밍

  • 레퍼런스 (Reference) : 간접 참조를 위한 타입
    • 포인터의 불편함
  • 오버로딩(overloadding) : 함수를 작성할 때 동일한 이름을 적는 것
    • 단, 매개변수 목록이 달라야 한다.
    • 연산자도 오버로딩 가능
  • 클래스 : 객체의 청사진

    • 클래스 타입 : struct, class, union
    • c언어에서 구조체 만드는 것과 문법은 비슷함.
    • 클래스 내부에서 선언된 식별자는 클래스 범위(Class Scope)를 가진다.
    • 멤버에는 변수랑 함수를 가질 수 있다.
    • 클래스를 바탕으로 메모리에 표현한 것을 인스턴스(Instance)라고 한다.
  • 기본 메소드

    • 생성자 : 초기화를 담당하는 특수한 메소드

      • 기본 생성자는 아무런 생성자도 정의하지 않았을 때 컴파일러가 자동으로 합성한다.
    • 복사 생성자

      • 첫 번째 매개변수로 동일한 타입의 레퍼런스를 가지는 생성자
      • 복사 생성자도 정의하지 않으면 기본으로 합성된다.
    • 소멸자 : 자원 정리를 담당하는 특수한 메소드

      • 소멸자를 정의하지 않았을 때 컴파일러가 자동으로 합성한다.
    • explicit 한정자 : 매개변수가 하나인 생성자가 있다면, 그 매개변수 타입에서 클래스 타입으로의 암시적 변환이 일어나는데, explicit 한정자를 사용하면 암시적 변환을 막을 수 있다.

      • 암시적 변환은 오류의 온상이다.
    • 복사 할당 연산자 : 하나의 매개변수만 가지며, 그 매개변수의 타입이 클래스 타입과 동일한 할당 연산자.
      복사 할당 연산자의 용도는 = 연산자를 이용해서 복사하기 위함.

    • 기본 메소드에 대해서는 default나 delete 키워드를 사용해서 컴파일러가 생성해주는 함수를 사용하거나 삭제 가능

  • 상속은 클래스를 정의할 때 {자식 클래스} : {부모 클래스} 형태로 쓰면 된다.
    • 상속을 할 때 접근 한정자를 같이 써줄 수 있음.
    • public 상속 : 부모 클래스의 public 멤버를 자식 클래스의 public 멤버로 가져오고, 부모 클래스의 protected 멤버를 자식 클래스의 protected 멤버로 가져옴.
    • 상속 관계가 있는 객체는 생성은 부모에서 자식 순으로, 소멸은 자식에서 부모 순으로 이뤄짐.
    • protected 한정자 : 클래스 내부 및 자식 클래스만 접근 가능
    • C++은 다중 상속이 가능하지만 죽음의 다이아몬드를 겪지 않도록 조심해서 다뤄야 함.
  • 가상 함수 : 다형성을 가질 수 있는 함수
    • 메소드 앞에 virtual 한정자를 붙이면 됨.
    • 가상 함수를 재정의하는 것을 오버라이딩(Overriding)
    • 가상 함수를 이용해 다형성을 사용하려면 업캐스팅(Upcasting)해서 상위 타입의 포인터 혹은 레퍼런스로 하위 타입의 객체를 다뤄야 함.
      • 업캐스팅 때문에 실제로 내가 어떤 타입의 객체를 가리키는 지 모름. 그래서 다형성이 적용되는 것.
      • 가상 함수는 동적 바인딩이며, 가상 함수 테이블과 가상 함수 포인터를 이용해 동작

오버로딩(Overloadding)

// foo라는 동일한 식별자를 사용하고 있지만
// 매개변수 목록이 (int), (double)로 서로 다르다.
int foo(int) { return 1; }
int foo(double) { return 1; }


// 아래처럼 반환형이 다른 경우는 오버로딩이 되지 않는다.

int foo(int) { return 1; }
double foo(int) { return 1.0; } // 컴파일 오류!
  • 오버로딩 된 함수에서 실제로 호출해야 할 함수를 바인딩하는 것은 꽤 복잡하다. -> 범위 외

기본 메소드(Default Method)

explicit 한정자

  • explicit : 명시적 / implicit : 암시적
  • 매개변수가 하나인 생성자를 만들 때 주의할 점이 있다. 그것은 암시적 변환이 일어난다는 점이다.
  • 하지만 아래의 예시를 보면 오버로딩 등에 따라 의도치 않은 결과가 나올 수 있다.
class A
{
public:
    A(int data)
     : _data(data) { }
   
private:
    int _data;
};
 
void Foo(A a);
// void Foo(int);

// int 타입에서 A 타입으로의 암시적 변환이 일어남.
Foo(10); // int 타입의 10에서 A 타입의 10으로 암시적 변환이 일어남.
// void Foo(int); 함수가 있다면 의도하지 않은 결과가 일어날 수 있음.
// Foo(int)가 호출되게 된다. // 
  • 이를 방지하기 위해 explicit 한정자를 사용할 수 있다.
  • 생성자 앞에 explicit 한정자를 붙임으로써, 암시적 변환을 막는다.
    class A
    {
    public: 
    		    // 더 이상 암시적 변환이 불가능하며 모든 코드에서 명시적으로
    	// 생성자를 호출해야 한다.
        explicit A(int data)
            : _data(data) { }
     
    private:
        int _data;
    };
     
    void Foo(A a);
     
    Foo(10); // 컴파일 오류! 암시적 변환 불가능.
    Foo(A(10)); // 사용 가능. 명시적으로 표기해줌.
    • explicit 한정자를 사용하면 암시적 변환으로 인해 발생할 수 있는 오류를 줄일 수 있다. 즉, 좀 더 안전한 코드를 작성할 수 있다.

복사 할당 연산자(Copy Assignment Operator)

상속

  • C++에서 상속을 하고 싶다면 클래스를 정의할 때, 뒤에 콜론을 붙이고 상속할 클래스를 적어주면 된다.

  • 상속 방법은 언어마다 다르다.

  • 상속할 클래스를 적을 때 접근 지정자를 함께 적어줄 수 있다.

  • 상속 클래스의 디폴트 접근 지정자는 private이니, public을 적어주는 걸 잊지 말 것.

    class Base
    {
        // 생략
    };
     
    // 상속할 클래스를 : 뒤에 적어준다.
    class Derived : public Base
    {
        // 생략
    };
  • 상속을 받을 때는 객체의 생성과 소멸 순서가 어떻게 되는지 명확히 알고 있는 것이 중요하다.

class Base
{
public:
    Base() { std::cout << "Base Constructor\n"; }
    ~Base() { std::cout << "Base Destructor\n"; }
private:
	int _data;
};
     
// 상속할 클래스를 : 뒤에 적어준다.
class Derived : public Base
{
public:
    Derived() { std::cout << "Derived Constructor\n"; }
    ~Derived() { std::cout << "Derived Destructor\n"; }
private:
	double _data;
};
     
int main()
{
     sizeof(Derived); // 16 // 패딩이 들어간다.
     // 메모리 표시
     // [ Base::_data ][ padding ][ Derived::_data ]
     // 생성은 부모 클래스부터 자식 클래스 순으로 하게 된다.
     Derived d;
 } // 소멸은 반대로 자식 클래스에서 부모 클래스 순으로 하게 된다.

protected 접근 지정자

  • protected 접근 지정자는 클래스 내부 및 자식 클래스에게 접근 권한을 부여한다.
class Base
{
protected:
   int _baseData;
     
private:
   int _privateData;
};
     
// 상속할 클래스를 : 뒤에 적어준다.
class Derived : public Base
{
public:
    void Foo()
    {
        _baseData = 10; // 접근 가능
        _privateData = 20; // 컴파일 오류! private은 자식 클래스에서도 접근 불가능
    }
};

public 상속의 의미

  • public 상속은 부모 클래스의 모든 public 멤버를 자식 클래스의 public 멤버로 만들고,
    부모 클래스의 모든 protected 멤버를 자식 클래스의 protected 멤버로 만든다.

    • 즉, 접근 불가한 private 이외의 모든 멤버를 그대로 받아들인다.

다중 상속

  • 상속할 클래스가 꼭 한 개일 필요는 없다. 여러 개를 상속 받는 것도 가능하다.
class Base1 { };
class Base2 { };
     
// 다중 상속을 할 때는 ,로 적어주면 된다.
class Derived : public Base1, public Base2
{
   // 생략
};
// 죽음의 다이아몬드 현상 예시 코드
    
class Base1
{
public:
   void Foo() { std::cout << "Base1의 Foo()\n"; }
};
     
class Base2
{
public:
    void Foo() { std::cout << "Base2의 Foo()\n"; }
};
     
class Derived : public Base1, public Base2
{
     
};
     
Derived d;
d.Foo(); // 컴파일 오류! Base1::Foo()와 Base2::Foo() 중 누굴 호출할 것인지 모호함.
  • 이러한 이유로 다중 상속을 지원하지 않는 언어도 많다.

가상함수(Virtual Fuction)

  • 가상 함수(Virtual Function)는 다형성을 지원하기 위한 기능이다.

  • 가상 함수를 작성하려면 메소드 앞에 virtual 한정자를 적는다.

  • https://en.cppreference.com/w/cpp/language/virtual

  • 가상 함수의 내용을 재정의하는 것을 오버라이딩(Overriding)이라고 한다.

  • 부모 클래스 타입을 가리키는 포인터나 레퍼런스로 업캐스팅(upcasting)하여 다뤄야 다형성이 적용된다.

  • 업캐스팅(upcasting) : 자식 클래스 타입에서 부모 클래스 타입으로 변환하는 것을 말한다. 반대는 다운캐스팅(downcasting)이다.

    • 이 업캐스팅 때문에 이 가상 함수가 실제로 내가 어떤 타입의 객체를 가리키는 지 모르게 된다.
    • 그래서 여러가지 형태를 취할 수 있는, 다형성이 적용되는 것이다.
class Base
{
public:
	// 가상 함수는 앞에 virtual 키워드를 붙이면 된다.
    // Foo()는 가상 함수다.
    virtual void Foo()
    {
        std::cout << "Base::Foo()\n";
    }
};
     
class Derived : public Base
{
public:
   // 가상 함수 재정의는 그냥 정말 재정의를 해주면 된다.
   void Foo()
   {
        std::cout << "Derived::Foo()\n";
   }
};
    
int main()
{
 	Derived d;
   	Base b;
    	
   	Base& br = d; // upcasting : 부모클래스 타입으로 암시적 변환
   	Base* bp = &d; // upcasting
    	
   	br.Foo();
   	bp->Foo();
    	
   	Derived& d2 = b; // 다운캐스팅 : 컴파일 에러, 명시적 변환 필요
   	Derived& d2 = (Derived&) b;
}
    
  • 어떻게 동작하는 것일까?

  • 함수는 컴파일 시간에 어떤 함수를 호출할 것인지 결정하는 정적 바인딩(Static Binding)인 반면,

  • 가상 함수는 실행 시간에 어떤 함수를 호출할 것인지 결정하는 동적 바인딩(Dynamic Binding)으로 동작한다.

  • 동적 바인딩을 위해 가상 함수의 주소가 저장되어 있는 가상 함수 테이블(vtbl; Virtual Function Table)이 각 타입마다 존재하게 된다.

  • 각 인스턴스마다 자기 타입의 가상 함수 테이블을 가리키는 가상 함수 포인터(vptr; Virtual Function Pointer)를 갖고 있게 된다.

  • 2022. 05. 31 가상 함수 예시 코드

#include <iostream>
 
using namespace std;
 
struct Base
{
  // Base 타입의 가상 함수 테이블은 아래와 같이 구성된다.
  // vftable[0] = Base::Foo
  // vftable[1] = Base::Boo
  virtual void Foo() { cout << "Base Foo\n"; }
  virtual void Boo() { cout << "Base Boo\n"; }
 
  // Coo는 인스턴스 메소드이기 때문에
  // 가상 함수 테이블에 들어가지 않는다.
  void Coo() { cout << "Base Coo\n"; }
 
// 가상 함수가 존재하기 때문에 아래와 같이
// 모든 인스턴스는 가상 함수 포인터를 갖게 된다.
// private: void** __vfptr = Base::vftable;
 
// 가상 함수 포인터는 인스턴스가 생성될 때 초기화 되며
// 초기값은 각 타입의 가상 함수 테이블이 된다.
};
 
struct Derived : Base
{
  // Derived의 가상 함수 테이블은 아래와 같이 구성된다.
  // vftable[0] = Derived::Foo
  // vftable[1] = Base::Boo
  // vftable[2] = Derived::Aoo
  void Foo() { cout << "Derived Foo\n"; }
  virtual void Aoo() { cout << "Derived Aoo\n"; }
 
  void Coo() { cout << "Derived Coo\n"; }
};
 
Base b;
Base* p = &b;
p->Foo(); // Base Foo
p->Boo(); // Base Boo
p->Coo(); // Base Coo
 
Derived d;
p = &d;
p->Foo(); // Derived Foo
p->Boo(); // Base Boo
 
// 가리키고 있는 타입이 Base이므로 Base::Coo가 호출된다.
p->Coo(); // Base Coo.
 
// 다운캐스팅을 하여 Derived::Coo가 호출된다.
((Derived*)p)->Coo(); // Derived Coo

메소드 작성 팁

  • 클래스의 메소드에는 보통 멤버 변수에 접근하기 위한 접근자(Getter)와 설정자(Setter) 메소드를 만든다. 왜 바로 접근하지 않고 따로 메소드를 만드는가?

    1. 데이터 은닉에 위반 ⇒ 객체 지향적 설계에 맞지 않음.

      a. 데이터 은닉(Data Hiding) : 구현 세부 사항에 사용되는 데이터를 외부에 노출하지 않는 것 - 추상화

    2. 디버깅이 어려워짐.

    3. 확장성이 떨어짐.

0개의 댓글