11.2주차
실습 6
문제 설명
이 문제에는 표준 입력으로 두 개의 정수 n과 m이 주어집니다.
별(*) 문자를 이용해 가로의 길이가 n, 세로의 길이가 m인 직사각형 형태를 출력해보세요.
제한사항
n과 m은 각각 1000 이하인 자연수입니다.
입출력 예
- 입력: 5 3
- 출력
***** ***** *****
내가 제출한 코드
#include <iostream>
using namespace std;
int main(void) {
int a;
int b;
cin >> a >> b;
for (int i = 0; i < b; i++)
{
for (int j = 0; j < a; j++)
{
cout << '*';
}
cout << endl;
}
return 0;
}
교수님 코드
#include <iostream>
using namespace std;
// 기본적인 기능을 만들도록 하겠다.
class Polygon
{
protected:
int m, n;
public:
Polygon(int m, int n);
void draw();
};
Polygon::Polygon(int m, int n)
{
this->m = m;
this->n = n;
}
void Polygon::draw()
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < m; j++)
{
cout << "*";
}
cout << endl;
}
}
int main(void)
{
int a;
int b;
cin >> a >> b;
Polygon pol(a, b);
pol.draw();
return 0;
}
사실 이정도로만 한다면 위 문제의 답은 나온다. 근데 이거를 main에서 한 번에 할 수 있는데 왜 굳이 클래스화 하는거야?
클래스화 했을 때의 장점
=> 확장성이 있다.
지금 Polygon 클래스를 통해서 다른 방식의 그림을 그리고 싶어. 그럼 draw() 함수에 대한 정의가 다 달라져야되는데, 그러기 위해서는 반드시 클래스 정의가 필요하다.
main
int main(void)
{
int a;
int b;
cin >> a >> b;
Polygon pol(a, b);
pol.draw();
return 0;
}
virtual
장점 1. 반드시 자식 클래스에게 의무를 부여한다.
#include <iostream>
using namespace std;
class Polygon
{
protected:
int m, n;
public:
Polygon(int m, int n);
virtual void draw() = 0; // 🌟
};
class Rect : public Polygon
{
public:
Rect(int m, int n);
void draw(); // 오버 라이드
};
class Triangle : public Polygon
{
public:
Triangle(int m, int n);
void draw();
};
Polygon::Polygon(int m, int n)
{
this->m = m;
this->n = n;
}
Rect::Rect(int m, int n) : Polygon(m, n)
{
}
void Rect::draw()
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < m; j++)
{
cout << "*";
}
cout << endl;
}
}
Triangle::Triangle(int m, int n) : Polygon(m, n)
{
}
void Triangle::draw()
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < i; j++)
{
cout << "*";
}
cout << endl;
}
}
int main(void)
{
int a;
int b;
cin >> a >> b;
Rect rect(a, b);
rect.draw();
return 0;
}
virtual void draw() = 0;
를 Polygon 클래스에서 정의를 했다는 것은 함수에 대해서 정의를 하지 않는다는 말이다.
Polygon pol(a, b)
에서는 에러가 뜬다. 왜죠?
=> 멤버 함수에 대해서 선언만하고 정의를 하지 않은 추상 클래스를 만들었기 때문이다. 추상 클래스는 정의가 없고 선언만 있는 멤버 함수가 존재하는데, 이런 추상 클래스는 인스턴스를 만들지 못한다. 따라서 Polygon은 멤버 함수에 대한 정의를 하지 않았기 때문에 Polygon 클래스에 대해서 인스턴스를 만들 수 없다.
여기서 인스턴스를 만들 수 없다는 것은 반드시 추상 클래스인 virtual ~ = 0;
꼴인 경우만 그렇다.
그럼 오히려 나쁜거 아니야? 추상 클래스가 없는게 좋은 거 아니야?
=> 만약에 함수 선언 부분에서 draw() 함수의 선언 부분을 지우게 된다면 Polygon 클래스에 대해서 인스턴스를 만들 수 있다. 근데 이렇게 된다면 Rect, Triangle 클래스의 draw 함수는 오버라이드가 아니다. 부모 클래스에 draw()에 대한 함수 선언이 없기 때문이다. 그래도 코드 자체는 돌아가니까 좋을 것같지만 그렇지 않다.
구현하지도 않고, 인스턴스도 못 만드는데 왜 적어죠?
=> 가상 함수를 만들게 되면 이 클래스는 인스턴스를 만들려는 목적이 아니라 훗날 상속받을 클래스의 의무를 나열하는 것이다. 나를 상속받은 클래스는 반드시 draw 함수를 가지고 있어야 한다라는 의무를 자식 클래스에게 넘기게 되는 것이다. 따라서 virtual void draw() = 0;
를 통해서 Rect, Triangle이 반드시 draw() 함수를 정의를 해야 됨을 말한다. 만약 draw() 함수를 정의하지 않는다면 여전히 추상 클래스인 상태가 되고, 그렇게 되면 인스턴스를 만들 수 없다. 인스턴스를 만들려면 에러가 발생한다.
가상 함수를 통해서 자식 클래스에 함수 정의에 대한 의무를 반드시 준다.
추상 클래스를 만든다는 것은 항상 상속을 염두해둔다라는 의미이다. 자기를 상속받은 클래스가 있을 거슬 염두해서 피피티에서는 abstrack Base 클래스 라고 나온 것이다.
추상적인 명세만 나열했지 실질적인 구현에 대해서는 오픈되어 있다. 그래서 추상 클래스라고 부르는 것이다.
자바에서는 인터페이스를 통해서 반드시 함수 선언만을 가지고 있는 클래스가 있다.(멤버 변수도 사용할 수 없다. 선언과 동시에 정의가 되기 때문에) 하지만 C++에서는 멤버 변수도 정의할 수 있고, 가상 함수와 일반 함수와 같이 사용할 수 있다.
(물론 void draw();
를 선언하고 정의를 한 다음 자식 클래스에서 오버 라이드 하는 방법도 있지만 이 부분의 경우는 자식 클래스에게 정의에 대한 의무를 부여하는 것은 아니다. 결국 부모 클래스의 함수를 사용할 수 있기 때문에.)
장점 2. 상속의 힘이 두두두배가 돼!
#include <iostream>
using namespace std;
class Polygon
{
protected:
int m, n;
public:
Polygon(int m, int n);
virtual void draw(); // 🌟
virtual ~Polygon();
};
class Rect : public Polygon
{
public:
Rect(int m, int n);
~Rect();
void draw(); // 오버 라이드
};
class Triangle : public Polygon
{
public:
Triangle(int m, int n);
~Triangle();
void draw();
};
Polygon::Polygon(int m, int n)
{
this->m = m;
this->n = n;
}
Polygon::~Polygon()
{
cout << "Polygon::~Polygon()" << endl;
}
Rect::Rect(int m, int n) : Polygon(m, n)
{
}
void Rect::draw()
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < m; j++)
{
cout << "*";
}
cout << endl;
}
}
Rect::~Rect()
{
cout << "Rect::~Rect()" << endl;
}
Triangle::Triangle(int m, int n) : Polygon(m, n)
{
}
Triangle::~Triangle()
{
cout << "Triangle::~Triangle()" << endl;
}
void Triangle::draw()
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < i; j++)
{
cout << "*";
}
cout << endl;
}
}
int main(void)
{
int a;
int b;
int type;
cin >> type >> a >> b;
Polygon *pol;
if (type == 3)
{
pol = new Triangle(a, b);
}
else if (type == 4)
{
pol = new Rect(a, b);
}
pol->draw();
delete pol;
return 0;
}
>>
Triangle::~Triangle()
Polygon::~Polygon()
pol은 Polygon 클래스형이지만 Rect과 Triangle을 다 가리킬 수 있다. 왜? 결국 자식 클래스는 부모 클래스를 가지고 있으므로. 근데 여기서 draw()를 해보게 된다면 Rect, Triangle 상관없이 무조건 Polygon의 draw() 가 작동이 된다. 이거는 너무 당연한거야. 결국 데이터형은 Polygon이니까.
근데 사실 프로그래머 입자에서는 부모 클래스의 함수를 작동하는 것이 아니라 자식 클래스의 함수를 작동시키고 싶을 확률이 높다.
이런 문제점을 해결하기 위해서는 왜 이런 문제가 발생했는지 좀더 구체적으로 살펴봐야 된다. draw()라는 것은 컴파일과 링킹 과정에서 Polygon 에 맞는 draw()로 링킹이 되기 때문에 이런 문제가 발생했던 것이다. 하지만 사용자가 입력을 하는 것은 컴파일과 링크가 마무리된 후 런타임에서 받는다. 그렇기 때문에 결국 오버라이드 된 자식 클래스의 함수를 사용할 수 없던 것이다.
이를 해결하기 위해서 virtual void draw();
를 사용한 것이다. (일단 위 코드는 장점 1에서의 virtual void draw() = 0;
과는 전혀 다르다. 헷갈리지 말것!) 이때의 virtual 역할은 컴파일과 링킹 타임에서 함수가 결정되지 않고, 런타임 시에 결정된다. 실제 포인터가 가리키는 원래의 object(자식 클래스)가 오버라이드 한 함수를 쫓아가서 호출해준다. 이러한 함수를 virtual 함수라고 부른다.
이렇게 된다면 상속의 효과가 배가 된다. 상속의 힘!! 자바는 무조건 virtual로 통일했기에 무조건 자식 클래스의 오버라이드 된 함수를 사용한다.
+a.
추상 베이스 클래스는 상속에 대해서 가이드라인을 잡는다. 위의 두 장점에서 더 나아가서 delete pol에 대해서 배웠다. 이거는 너무 자연스러운 일인데, 결국 new를 통해서 동적 할당이 되었기 delete는 해주어야 되는데, 찍힌거 보면 Polygon이 찍혀있다. (~Polygon();) 일 경우,, 근데 우리는 virtual을 사용해서 draw를 할 때, 자식 클래스의 함수를 사용할 수 있도록 해두었다. 결국 동적 할당된 클래스는 Rect이나 Triangle이라는 것이다. 근데 Polygon을 소멸시키는 것은 뭔가 좀 이상하다.
따라서 가상 함수를 사용해서 자식 클래스의 오버라이드된 함수를 사용한다면 소멸자에도 반드시 virtual을 붙혀서 런타임시 링킹된 객체를 소멸해주어야 한다. virtual을 사용해서 소멸하게 된다면 자식 클래스부터 위로 올라가면서 하나씩 소멸된다.
질문
-
virtual 소멸자 부분 -> Rect를 소멸하고, 폴리곤을 소멸하는데, 이것도 virtual 때문인건가? 지난 시간에는 높은 클래스부터 차곡차곡 사라진다고 했던 것 같은데 ???????????
=> 놉!!! 생성되는 것은 부모 클래스부터 하나씩, 소멸되는 것은 자식 클래스부터 하나씩! -
void draw() = 0 -> 추상 클래스 만들기, virtual void draw(); -> 런타임시의 함수를 결정 <-> 근데 아까 void draw() = 0만 했을 때 안되는 이유?
=> 문법 기준이 강화된 듯. 근데 위에 추상 클래스를 만드는 것과 가상 함수를 만드는 코드가 살짝 다른 것은 맞다. -
다형성 => 가상 함수, 함수 템플릿, 함수 오버로드, 연산자 오버로드로 구현한다..
=> 다형성: 개념은 하나인데, 형식이 다양하다. 라는 뜻이다. 결국 다형성을 구현하기 위해서 가상 함수, 함수 템플릿, 함수 오버로드, 연산자 오퍼레이터가 사용된다. => 이해 됨!! ㅇㅋㅇㅋ
Multiple Inheritance
스타킹,,,, ㅎㅎㅎㅎ 다중 상속을 하게 되면 좋지 않을까? 두 개의 기능이 다 되니까?
#include <iostream>
#include <string>
using namespace std;
class Polygon
{
protected:
int m, n;
public:
Polygon(int m, int n);
virtual void draw();
virtual ~Polygon();
};
void Polygon::draw()
{
cout << "default" << endl;
}
class Rect : public Polygon
{
public:
Rect(int m, int n);
void draw(); // 오버 라이드
~Rect();
};
class Triangle : public Polygon
{
public:
Triangle(int m, int n);
void draw(); // 오버 라이드
~Triangle();
};
Polygon::Polygon(int m, int n)
{
this->m = m;
this->n = n;
}
Polygon::~Polygon()
{
cout << "Polygon::~Polygon()" << endl;
}
Rect::Rect(int m, int n) : Polygon(m, n)
{
}
Rect::~Rect()
{
cout << "Rect::~Rect()" << endl;
}
void Rect::draw()
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < m; j++)
{
cout << "*";
}
cout << endl;
}
}
Triangle::Triangle(int m, int n) : Polygon(m, n)
{
}
Triangle::~Triangle()
{
cout << "Triangle::~Triangle()" << endl;
}
void Triangle::draw()
{
for (int i = 0; i < n; i++)
{
for (int j = 0; j < i; j++)
{
cout << "*";
}
cout << endl;
}
}
// 다중 상속 클래스
class MyMulti : public string, public Rect
{
public:
MyMulti(int m, int n, const char *str);
};
MyMulti::MyMulti(int m, int n, const char *str) : Rect(m, n), string(str)
{
}
int main(void)
{
int a;
int b;
int type;
cin >> type >> a >> b;
Polygon *pol;
if (type == 3)
{
pol = new Triangle(a, b);
}
else if (type == 4)
{
pol = new Rect(a, b);
}
pol->draw();
delete pol;
MyMulti test(5, 3, "abc");
cout << test << endl;
test.draw();
}
상속의 장점이 뭐야
=> 부모 클래스의 이름으로 자식 클래스를 다 데리고 다닐 수 있어. 상속 관계로 클래스를 설계하면 하나의 타입으로 모든 객체를 가리킬 수 있으니까. 예를 들어서 Polygon이라는 이름으로 Rect과 Triangle을 모두 데리고 갈 수 있다.
C++에서는 상속 설계는 컴파일러가 전혀 관여하지 않는다. 상속 설계는 오로지 프로그래머의 역량이다. 몇 수를 내다보는 설계를 해야된다 .
자바에서는 컴파일러가 상속 설계에 관여를 한다. main도 클래스이 이므로 static을 붙혀서 main 인스턴스를 만들지 않아도 사용할 수 있다. 또한 object라는 디폴트 베이스 클래스가 있어서 모든 클래스를 다 끌고 다닌다.
위의 예시로는 string 클래스를 상속 받았기 때문에 cout << test << endl;
라는 클래스를 사용해도 test 인스턴스 안에 있는 문자열이 출력된다.
다중 상속의 단점: 내가 상속 받으려고 하는 변수의 이름이 똑같다면 confusing이 발생한다. 어느 쪽의 것을 상속받을지에 대한 모호성이 내포되어 있다. C++은 다중 상속을 가능하게 하는 대신 모호성에 대한 문제는 프로그래머가 책임을 지도록 하고 있지만 자바의 경우는 다중 상속이 불가하다.
자바는 인터페이스만이 다중 상속이 가능한데 그 이유는 인터페이스에는 추상 함수만 선언이 가능(변수는 정의 불가) 따라서 변수가 겹칠일이 없다. 모호성 자체가 없다.
댓글남기기