cpp04

YP J·2022년 6월 23일
0

CppModule

목록 보기
5/9

다형성(Polymorpshism)

  • 다형성 이란 하나의 객체가 여라가지 타입을 가질수 있는것을 의미.

  • 객체지향 언어에서 서로다른 객체가 동일한 메세지에 대해 서로 다른 방법으로 응답할수 있는 기능.

  • 참조 변수의 다형성은 기반 클래스 타입의 참조 변수로 파생 클래스 타입의 객체를 참조할 수있도록 하는것( 업 캐스팅 )

  • 상속의 구조에서 다형성을 이루기 위해선 조건이 있다(컴파일, 런타임 가지 관점)

    • 컴파일타임 에서는 상속관계에 있는 클래스들의 함수와 연산자의 overloading 들이 적절히 이루어져야 하고
    • 런타임 에서는 virtual 의 적절한 overriding 을 통해 상속 관계에 있는 클래스들이 타입에 맞는 함수를 호출할 수 있어야 한다.
  • overloading 의 경우, 여러 시그니처로 구성된 함수들중 어느 함수를 고를 것인지 컴파일러의 규칙에 의해서 컴파일 타임에 결정된다.

  • overriding 은 런타임에 vTable에 유지되고 있는 virtual 로 명시된 함수에 대해 자신의 타입을 확인 하면서 적절한 함수를 결정 짓는다.

  • 즉, overloagin 은 컴파일// overriding은 런타임

  • 오버로딩(Overloading)은 매개변수의 개수 또는 타입이 다르게 하여, 같은 이름을 사용해서 메소드를 여러개 정의할 수 있는 것이다.
  • 오버라이딩(Overriding)은 상위 클래스가 가지고 있는 메서드를 하위 클래스가 재정의해서 사용하는 것이다.

업 캐스팅

  • 기초 클래스를 상속한 파생 클래스에서 overriding 하려는 함수들을 virtual로 선언할 필요가 있다.

  • virtual로 선언 하지 않는다고 해서 기초 클래스에 존재하는 함수를 파생 클래스에서 overriding하지 못하는것은 아니지만

  • 그럼에도 그렇게 하는 이유는 업 캐스팅 때문이다.

  • 상속이 이루어졌을때, 기초 클래스들과 파생 클래스들은 기초 클래스의 포인터로 묶어서 관리하는것이 가능하다.

  • 이를 업캐스팅이라고 한다.

  • 이때 파생클래스를 업캐스팅하여 기초 클래스 포인터로 파생 클래스의 overriding한 함수를 호출하려면 파생 클래스의 함수가 아니라 기초 클래스의 함수를 호출해여한다.

  • 이는 호출시 기초 클래스의 포인터를 이용하여 기초 클래스로 타입을 인식하면서 발생하는데

  • 이를 극복하여 파생 클래스 타입을 정상적으로 인식 시키기 위해선 런타임에 자신이 파생 클래스라는것을 알게 해주어야 한다

  • 그게 virtual키워드 이다.?

  • 클래스 상속의 구조가 일반적으로 기초 클래스가 위에 있고 파생클래스가 아래있는것처럼

  • 아래에서 위로의 클래스가 포인터로 캐스팅 되므로 업 캐스팅이라고한다

  • 반대로 위에서 아래 즉, 자식 에서 부모 클래스의 포인터로 캐스팅 하는것은 다운 캐스팅 이라한다

  • 이는 dynamic_cast 와 관련이 있다.

  • 멤버 함수들은 클래스 내에 귀속되는 엔트리를 갖는데

  • virtual 키워드로 선언된 함수들은 클래스 내에 엔트리를 갖지 않는다.

  • virtual 키워드로 선언된 함수들은 vTable(virtual table)에 자신의 타입과 묶여서 별도의 엔트리를 유지한다.

  • 따라서 런 타임 중에 해당 함수를 호출하려 했을때 클래스 내에 엔트리가 없는 것이 인식 되면 ,

  • vTable 에서 기초 클래스의 포인터가 실제로 어떤 타입인지 확인한 뒤에 타입에 맞는 함수를 호출한다.

    overriding 하려는 함수들을 virtual 로 선언하자 ~

    가상소멸자

  • 소멸자 역시 virtual로 선언하자~

  • 업 캐스팅과 관련

  • B클래스가 A클래스 상속 했다면

  • 정상적인 소멸 순서는 A 생성-> B생성 -> B 소멸-> A 소멸

  • 그러나 소멸자가 virtual이 아니면

  • 위의 순서에서 B소멸이 빠진다.

  • 런 타임에 기초 클래스의 포인터가 참조하는 값이 실제로 어떤 타입인지 확인하고 타입에 해당되는 소멸자를 올바르게 호출할 수 있게 만들어야 한다.

    Ploymorphic 클래스의 형변환

  • 형 변환 연산자중 dynamic_cast 연산자는 안정적인 형 변환을 보장한다.

  • But static_cast연산자는 무조건 형 변환이 되기 때문에 안정성을 보장 하지 못한다.

  • 상속관예에 놓여있는 두 클래스 사이에서, 유도 클래스의 포인터 및 참조형 데이터를 기초 클래스의 포인터 및 참조형 데이터로 형 변환할 경우에는 dynamic_cast 연산자를 사용해야한다.

  • 반대로 기초 클래스의 포인터 및 참조형 데이터를 유도 클래스의 포인터 및 참조형 데이터로 형 변환할 경우에는 static_cast 연산자를 사용 해야한다.

  • 하지만 기초 클래스가 Ploymorphic 클래스 이면 dynamic_cast 연산자도 기초 클래스의 포인터 밑 참조형 데이터를 유도 클래스의 포인터 및 참조형으로의 형변환을 허용함.

  • Polymorphic 클래스란?

  • 하나 이상의 가상함수를 지니는 클래스를 의미

  • 그러나 상속 관계에 놓여있는 두 클래스 사이에서,

  • 기초 클래스에 가상함수가 하나 이상 존재하면

  • dynamic_cast 연산자를 이용해 기초 클래스의 포인터 및 참조형 데이터를 유도 클래스의 포인터 및 참조형 데이터로 변환이 가능 .


    ex00

    Polymorphism
    Turn-in directory : ex00/
    Files to turn in : Makefile, main.cpp, *.cpp, *.{h, hpp}
    Forbidden functions : None


  • 정상적으로 virtual 키워드를 통해 overriding한 Animal, Cat, Dog 클래스들과 virtual 키워드 없이 overriding한 WrongAnimal, WrongCat 클래스들을 정의한다.

  • 그런 다음, Animal 클래스로 다형성을 이루는 객체와 WrongAnimal 클래스로 다형성을 이루는 객체의 업 캐스팅된 포인터를 확인해보면,

  • 자신의 타입으로 함수를 호출하는 Animal 클래스의 객체와 달리 WrongAnimal 클래스의 객체는 자신의 타입으로 함수를 호출하는 것이 아니라 항상 WrongAnimal 클래스의 함수를 호출한다.


const std::string &getType(void) const;

  • 반환값이 참조값이면 메모리 공간에 할당된 얘를 참조하는것이기 때문에 메인문 같은 밖에서 (만약 int 형으로 반환 했을때)++하게 되면 메모리가 할당 되어있기 때문에 ++ 연산이 되고 멤버 변수 값도 바뀌어 버린다 .private였어도.
  • 근데 그냥 반환하는 함수였으면 복사된 메모리 공간 확보 안된값이기 때문에 ++ 연산이 안 된다 (메모리 공간이 확보가 안 되어있으므로)
  • 대입 연산은 = , 복사를 하고 넣는형식이다 근데 참조자로 된 변수를 대입 연산 하면 복사는 일어나지 않고 그냥 그 값을 갖다 쓴다, 메모리에 접근해서
#include <iostream>

class Test
{
	private:
		int n;
	public:
		Test() :n(10) {}
		int &getN(void) { return this->n; }
};

int main()
{
	Test test;
	test.getN()++;
	std::cout << test.getN() << std::endl;
}
>> 11

int &getN(void) { return this->n; }

여기에서
const int &getN(void) {return this->n;} const;
하게 되면
위와 같은 메인 문에서 직접 멤버변수 값을 바꾸는 것을 막을수 있다
함수 앞에 있는 const.는 반환값이 참조자 일때만 유호하다.

함수 뒤에 있는 const 는 함수 내부에서 변수가 변하지 않게하는것.

ex01

  • Brain이라는 클래스를 새로 정의하여 ex00에서 정의한 Cat과 Dog 클래스의 private 멤버 변수로 Brain 포인터를 갖게한다.

  • 클래스 내에 멤버 변수를 포인터로 선언한 것이 있고 동적 할당을 해주었다면, 포인터에 할당된 메모리를 적절하게 해제할 수 있도록 신경을 써야한다.

  • Cat과 Dog 클래스의 경우, 생성자 또는 대입 연산자를 통해 할당받은 Brain 객체를 각 클래스의 소멸자에서 delete 연산을 통해 할당된 메모리를 해제 해주어야 한다.

  • 또한 포인터를 멤버 변수로 두었을 경우, 복사 생성자와 대입 연산자를 직접 정의하여 깊은 복사가 이루어지도록 해야한다.

  • 깊은 복사를 실행하는 함수를 직접 정의하지 않을 경우, 컴파일러에 의해 자동으로 기본 복사 생성자와 기본 대입 연산자가 생성되어 얕은 복사가 이루어지게 된다.

  • 직접 하나하나 넣어 줘야 한다.

까먹을까봐 쓰는 동적 할당 되어있을시 자기자신 대입연산 하게 되었을때

  • delete 하고 다시 넣는데 자기자신이 인자로 들어 오면
    자기자신을 삭제 하므로 다음 대입 연산을 하게 되면 터진다.?
    그래서 동적할당 했을시에는 자기자신이 들어오면 그냥 return 하는 등의 예외 처리를 해줘야 한다.

Q: 파생클래스의 private변수를 기반 클래스 포인터로 사용할수 있으려면?

A:

  • 이번 문제에선 파생클래스인 Dog,Cat 의 private변수로 Brain * 를 만들고 생성자에서 new

ex02

추상클래스, 순수 가상함수

  • 순수 가상함수가 하나라도 있으면 그 클래스는 추상클래스다.
class Animal
{
	public:
    	Animal() {}
        virtual ~Animal() {}
		virtual void speak() = 0 // 순수 가상함수
}
//virtual로 선언된 함수에 0을 할당하여 해당 함수를 정의하지 않겠다는 것을 의미한다.
  • 순수 가상함수는 무조건 파생클래스에서 오버로딩해야만 한다.
  • 추상 클래스는 객체를 만들수 없다.
  • 죽, 다형성을 위한 유도 클래스에서의 생성을 제외하고 기초 클래스 직접 생성 불가능하다

즉, 순수 가상 함수를 갖고 있는 클래스는 그 자체로는 객체화 될 수 없기 때문에 유도 클래스에게 제공되는 Interface 역할을 한다. 이러한 클래스를 추상 클래스(Abstract Class)라고 한다.

ex03

다형성과 추상클래스 등 지금까직 배운 모든 개념을 활용해서 총 7개 클래스 정의 해야한다.

  1. AMateria
  2. AMateria를 인터페이스로 활용하여 상속하는 Ice와 3.Cure
  3. ICharacter
  4. ICharacter 를 인터페이스로 활용하여 상속하는 Chararcter
  5. IMateriaSource
  6. IMateriaSource 를 인터페이스로 활용하여 상속하는MateriaSource
  • Chararcter 는 사용자를 나타내고, 사용자는 AMateria를 인터페이스로 하는 Ice 와 Cure 을 소지할수 있다.

  • Ice와 Cure 같은 물질을 생성하기 위해서는 IMateriaSource 를 인터페이스로 하는 MateriaSource를 이용해야한다.

  • Chararcter는 최초에 최대 4개의 비어있는 Materia인벤토리를 갖고있고

  • 인덱스 0번에서 인덱스 3번까지 순서대로 장착한다.

  • 만약 인벤토리가 가득찬 상태에서 장착하려고 하거나,

  • 없는 Materia를 사용하거나 장착을 해제 하려고 하면 아무동작도 하지 않아야 한다.

전방선언 (Forward declaration) & 상호참조

  • 식별자를 정의하기전 식별자의 존재를 컴파일에 미리 알리는것으로 필요에 따라 함수, 변수, 클래스 등을 전방선언한다.

    컴파일 시간을 단축 시키며, 헤더포함 의존성을 줄여준다.

/* Core.hpp */
class  UtilClass;

class Core
{
	private:
    	UtilClass *util;
}
/* Core.cpp */
#include "UtilClass.h"
Core::Core() { util = new UtilClass; }
  • 사용 하려는 클래스를 .hpp에서 먼저 선언하고 .cpp에서 사용하려는 객체의 .hpp 를 include 하는 방식

  • 전방 선언 하려면 .hpp 에는 객체의 포인터만 사용해야 한다.

  • 이런 제한은 문법적으로 만들어 진게 아니라 프로그램이 실행되는 구조에 의한 것이다.

  • 모든 포인터는 4byte의 메모리를 필요로하므로 우선 메모리만 확보해 두면 runtime에 생성되는 개체의 주소값을 저장할 수 있게 됩니다.

  • 포인터가 아닌 객체의 인스턴스가 사용된다면 컴파일러는 클래스의 구조를 알아야 되므로 include 된 .hpp 파일이 필요하게 된다.

  • 전방선언을 사용해서 얻게 되는 이점은 컴파일 시간 단축이다.

  • 위 구조에서 전방 선언이 아니라 .hpp 를 include했다면 UtilityClass 가 수정될 때마다 컴파일로는 .hpp를 다시 분석하게 된다.

  • 만약 UtilityClass 가 다른 h를 포함하고 Core클래스가 또 다른 곳에서 사용하게 된다면 의존성이 증가되어 컴파일 시간이 점점 늘어나게 된다.

  • 또 전방선언은 A객체가 B를 사용하고 B객체가 A를 사용하는 상호참조 에서도 유용하게 사용 됩니다.

  • 그리고 API개발 등에서 불필요한 .hpp를 포함하지 않을 수 있다는 장점이 있다.


if

  • A.hpp, A.cpp. B.cpp B.hpp 에 각각 클래스의 헤더와 구현부가 나누어서 작성되어 있다고 하자.
  • 이때 A에서 B가 필요하고 ,B가 A가 필요한 상호 참조를 하는 경우 어떻게 전방선언과 include 를 사용해야하는지 생각해보자

if 방법1(default)

  1. A.hpp에 B를 전방선언한다.
  2. B.hpp에 A.hpp를 include 하고 , A를 전방선언한다.
    여기 까지 되었다면 A.cpp에서 A.hpp 를 include 하고 B의 멤버함수를 사용할때 문제가 발생한다.
  3. 따라서 A.cpp 에는 B.hpp를 include 해서 A클래스 구현하고,
    B.cpp에는 B.hpp만 include 하여 B클래스를 구현한다.

if 방법2 (요게 이해하기 쉽네)

  1. A.hpp에 B.hpp를 include 하고, B를 전방선언한다.
  2. B.hpp에 A.hpp를 include 하고, A를 전방선언한다.
  3. A.cpp에 A.hpp를 include 하여 A 클래스를 구현하고, B.cpp에 B.hpp를 include 하여 B 클래스를 구현한다.

Interface

  • 인터페이스는 특정 기능을 구현할 것을 약속한 추상 형식을 말 한다.
    출처: https://ehclub.co.kr/2136 [언제나 휴일:티스토리]
  • C++에는 인터페이스 형식이 없지만 순수 가상 메소드를 이용해 구현할수있다.

ㅁ Abstract class vs Interface class

  • 과제에서 순수 가상함수를 가지고는 있지만 interface 는 아닌경우
    • 추상클래스인 경우 클래스명에 A를 붙이고, 모든 메소드가 정의 없이 순수 가상함수로 이뤄진 것만 클래스명에 I를 붙임,

overloading : 함수명을 같지만 파라미터의 개수와 타입을 다른게하여 함수를 만드는 것.

overriding : 상속간에 함수를 재정의하는 것.

순수 가상함수 : 기반 클래스에서는 정의하지 않으며, 상속 받는 파생 클래스에서 무조건 Overriding(재정의)하도록 하는 함수.

추상 클래스 : 순수가상함수를 하나 이상 포함하는 클래스로 객체를 생성할 수 없다.

인터페이스 : 순수가상함수와 가상 소멸자로 이루어진 클래스이며 구현부가 없다.

ㅁ-1). 사용의도 차이점

추상클래스는 IS - A "~이다".

인터페이스는 HAS - A "~을 할 수 있는".

  • 이렇게 구분하는 이유는 다중상속의 가능 여부에 따라 용도를 정한 것 같습니다.
  • 자바의 특성상 한개의 클래스만 상속이 가능하여 해당 클래스의 구분을 추상클래스 상속을 통해 해결하고,
  • 할 수 있는 기능들을 인터페이스로 구현합니다.

ㅁ-2). 공통된 기능 사용 여부

  • 만약 모든 클래스가 인터페이스를 사용해서 기본 틀을 구성한다면... 공통으로 필요한 기능들도 모든 클래스에서 오버라이딩 하여 재정의 해야하는 번거로움이 있습니다. 

  • 이렇게 공통된 기능이 필요하다면 추상클래스를 이용해서 일반 메서드를 작성하여 자식 클래스에서 사용할 수 있도록 하면 된다.

  • 어!? 그러면 그냥 추상클래스만 사용하면 되는 거 아닌가요?

  • 하지만 추상클래스로 다중 상속을 받는 경우 설정이 복잡하기 때문에 인터페이스로 구현하는 것이 편하다.

  • 따라서 만약 각각 다른 추상클래스를 상속하는데 공통된 기능이 필요하다면?

  • 해당 기능을 인터페이스로 작성해서 구현하는게 편하겠죠?

concrete class

  • 추상클래스와 달리 모든 연산에 대한 정의가 구현되어있는 클래스

AMateria.hpp

#ifndef AMATERIA_HPP
# define AMATERIA_HPP

# include <iostream>
# include <string>
# include "ICharacter.hpp"

class ICharacter;

class AMateria
{
    protected:
        std::string type;
    public:
        AMateria(std::string const &type);
        virtual ~AMateria(void) {}

        std::string const &getType() const;
        virtual AMateria *clone() const = 0;
        virtual void use(ICharacter &target) { (void) target;}
};

#endif

Character.hpp

#include "ICharacter.hpp"

class Character : //public ICharacter
{
    private:
        std::string name;
        static const int inventory_size = 4;
        AMateria *inventory[Character::inventory_size];
        int n_of_equip;

        Character(void);
    public:
        Character(std::string name);
        Character(const Character &rhs);
        ~Character();

        Character &operator=(const Character &rhs);

        std::string const &getName() const;
        void equip(int idx);
        void use(int idx, ICharacter &target);
};
  • private 변수로 name과 인벤토리 배열을 가짐.

  • 주의해야할점

    1. 생성자에서 inventory 배열 목록 초기화. 각각 널 포인터 연결 해준다.
    1. 복사생성자, 대입연산자로 새로운 inventory목록 복사해 오는 경우 기존 inventory에 할당 돼있던 AMateria 객체는 해제(delete)해줘야 한다.
  • equip은 inventory 중 먼저 나오는 empty배열에 Materia 를 저장한다.

  • unequip은 inventory의 idx번째 Materia 를 NULL로 초기화 해줬다.

  • 이때 delete는 하지 않기 때문에 외부에서 사용시 포인터 주소를 먼저 복사해 뒀다가 delete를 따로 해줘야한다.

  • use는 AMateria 의 use함수를 사용하고 사용한 Materia를 unequip하는 식으로 구현.

MateriaSource.hpp

#include <iostream>
#include "IMateriaSource.hpp"

class MateriaSource : public IMateriaSource
{
    private:
        static const int materias_size = 4;
        int n_of_learned;
        AMateria *materias[MateriaSource::materias_size];

    public:
        MateriaSource(void);
        MateriaSource(const MateriaSource &rhs)
        ~MateriaSource(void);

        MateriaSource &operator=(const MateriaSource &rhs)

        void learnMateria(AMateria *m);
        AMateria *createMateria(std::string const &type);
};
  • IearnMateria로 탐색할 Materia를 배열에 저장한다.
  • createMateria로 배열에 있는 Materia를 생성하여 반환한다.
  • MateriaSource 또한 Materia * 복사가 이뤄질때 이전의 배열에 있는 원소들을 적절히 delete 한 후 복사가 이뤄저야한다.

Error

In file included from srcs/AMateria.cpp:13:
In file included from incs/AMateria.hpp:18:
incs/ICharacter.hpp:25:21: error: unknown type name 'AMateria'
        virtual void equip(AMateria *m) = 0;
  • AMateria.hpp 와 ICharacter가 서로의 헤더를 include해서 생기는 문제.

  • 위에서 소개한 전방 뭐시기 if방법2 를 쓰면

#ifndef ICHARACTER_CLASS_H
# define ICHARACTER_CLASS_H

# include "AMateria.hpp"

class AMateria;

class ICharacter
{
public:
	virtual ~ICharacter() {}
	virtual std::string const &getName() const = 0;
	virtual void equip(AMateria *m) = 0;
	virtual void unequip(int idx) = 0;
	virtual void use(int idx, ICharacter &target) = 0;
};

#endif
  • ICharacter클래스, AMateria클래스 각각 선언 전에 서로의 class선언해서 문제해결
profile
be pro

0개의 댓글