12.2주차
📌 이번 수업은 실습 8, 9, 10을 한 번에 클래스 상속을 사용하여 문제를 풀었다.
실습 8
1부터 입력된 n까지 숫자 중에서 소수를 찾는 문제
실습 9
입력된 n, m을 가지고 최대 공약수와 최소 공배수를 찾는 문제
실습 10
입력된 n에 대해서 약수를 모두 더하는 문제
교수님 문제 풀이
기본 include
#include <string>
#include <vector>
#include <iostream>
using namespace std;
<기본 상식?>
c++에서 멤버 함수는 보통 public으로 오픈이 되어있고, 멤버 변수의 경우는 private와 protected로 정의를 하는 것이 일반적이다.
근데 divide & conquer를 통해서 구현을 할 때, 함수 안에서 블랙 박스 역할을 했던 함수들은 private 또는 protected로 구현을 해도 된다. 굳이 오픈할 필요가 없는 것까지 오픈할 필요는 없다.
step 1. 공통적으로 필요할 것 같은 함수를 static을 이용해서 글로벌 함수를 만든다.
class Utility
{
public:
static bool isDiv(int a, int b);
static bool isPrime(int a);
};
bool Utility::isDiv(int a, int b)
{
return (a % b == 0);
}
bool Utility::isPrime(int a)
{
int i;
for (i = 2; i < a; i++)
{
if (isDiv(a, i))
{
break;
}
}
if (i == a)
{
return true;
}
else
{
return false;
}
}
소수인지 아닌지를 확인하기 위해서 구현을 하는 함수가 조금 특이(?) 했다. 왜냐, 나는 1은 소수가 아니고, 2, 3은 소수임을 먼저 조건문을 통해서 구현을 하고 나머지 숫자에 대해서 % 연산이 0인지 아닌지를 판별을 했는데,
교수님은 그냥 2부터 반복문을 돌리면서 마지막에 반복문을 끝나고 남은 숫자가 a와 같은지 다른지를 비교했다.
step 2. P 클래스 만들기
P8, P9, P10 클래스를 만들기 전에 Utility 클래스를 상속받은 P를 만들어주어서 P클래스로 P8, P9, P10을 묶고자 한다.
class P : public Utility
{
protected:
int n;
public:
P(int n);
virtual int solution() = 0;
};
P::P(int n)
{
this->n = n;
}
일단은 문제 8, 10번은 입력값이 하나이므로 하나의 멤버 변수를 만들어 준다.
여기서, P 클래스를 통해서 동적할당은 해주되 각 클래스에서 정의한 solution 함수를 사용할 수 있도록 해주기 위해서 virtual int solution() = 0;
을 해준다. = 0
을 붙혔다는 것은 현 클래스에서는 구현을 해주지 않는데, 반드시 자식 클래스에서는 구현을 반드시 해야되도록 만드는 방식이다.
step 3. P8 클래스 만들기
소수를 구하는 문제이다.
class P8 : public P
{
public:
P8(int n);
virtual int solution();
};
P8::P8(int n) : P(n)
{
;
}
int P8::solution()
{
cout << "P8 solution" << endl;
int count = 0;
for (int i = 2; i <= n; i++)
{
// if (Utility::isPrime(i)) // 다른 클래스의 함수를 사용하는 경우
if (isPrime(i)) // 상속받은 크래스를 사용한는 경우
{
count++;
}
}
return count;
}
P 클래스를 상속을 받았으므로 멤버 변수는 P 클래스의 생성자를 사용하면 된다.
그리고 P 클래스를 통해서 동적 할당을 할 예정이므로 자식 클래스에서 정의된 함수가 사용이 되도록 하기 위해서 virtual int solution();
을 사용했다.
❓근데, 여기서 P10이랑 P9에도 virtual을 붙혀야 될까? P8의 경우 P10도 상속을 받고, P10에서 오버라이드한 함수를 사용하기 위해서 멤버 함수에 virtual을 붙혔다하더라도, P10과 P9는 필요가 없을 것 같은데? 굳이?
🅰️ 여기서 virtual을 붙히지 않게 된다면 상속의 힘이 사라져서 나중에 P 클래스로 묶은 상태로 오버라이드한 solution() 함수를 사용할 수 없다.
step 4. P10 클래스 만들기
입력받은 숫자의 약수를 모두 더하는 문제이다. 일단 멤버 변수가 한 개인 문제 먼저 구현
class P10 : public P8
{
public:
P10(int n);
virtual int solution(); //
};
P10::P10(int n) : P8(n)
{
;
}
int P10::solution()
{
cout << "P10 solution" << endl;
int sum = 0;
for (int i = 1; i <= n; i++)
{
if (isDiv(n, i))
{
sum += i;
}
}
return sum;
}
계층 관계가 Utility > P > P8 > P10 구조가 되었다. 따라서 P10의 경우는 Utility에서 정의했던 함수, P 생성자를 모두 사용할 수 있다.
그렇기 때문에 isDiv를 namespace 정의 없이 사용이 가능한 것이다.
step 5. P9 클래스 만들기
P9의 경우는 최대 공약수, 최소 공배수를 구해야 되는 문제이므로 일단은 P9에서 최대 공약수를 구하고, P92에서 최소 공배수를 구하는 식으로 문제를 풀었다.
// 최대 공약수 리턴
class P9 : public P
{
protected:
int m;
public:
P9(int n, int m);
virtual int solution();
};
// 최소 공배수 리턴
class P92 : public P9
{
public:
P92(int n, int m);
virtual int solution();
};
P9::P9(int n, int m) : P(n)
{
this->m = m;
}
int P9::solution()
{
cout << "P9 solution" << endl;
int gcd = 1;
for (int i = 2; i <= n; i++)
{
if (isDiv(n, i) && isDiv(m, i))
{
gcd = i;
}
}
return gcd;
}
P92::P92(int n, int m) : P9(n, m)
{
}
int P92::solution()
{
cout << "P92 solution" << endl;
int gcd = P9::solution();
return n * m / gcd;
}
P9는 멤버 변수가 한 개인 P 클래스를 상속받았으므로 일단 n에 대해서는 부모 생성자를 사용해서 초기화를 시켜주고, m에 대해서는 따로 초기화를 시켜준다.
그 다음 isDiv()를 사용해서 둘 다 나누어 떨어지는 함수를 사용해서 최소 공배수를 구한다.
P92의 경우는 P9 클래스를 상속받아서 부모 생성자를 통해서 n, m을 초기화 시킨다. 그리고 최소 공배수를 구하는 solution() 함수를 만들어야 되는데, 이때 부모 클래스의 solution() 함수(최대 공약수를 구하는 함수)가 필요하다.
P92에서는 상속받은 solution() 함수를 오버라이드 해서 사용하려고 하는데, 부모 클래스의 solution() 함수를 사용하고자 한다면 P9::solution()
을 통해서 사용하면 된다.
main 함수
int main()
{
P *first, *second;
first = new P8(12);
second = new P8(5);
cout << "P8, 12: " << first->solution() << endl;
cout << "P8, 5: " << second->solution() << endl;
delete first;
delete second;
first = new P10(12);
second = new P10(5);
cout << "p10, 12: " << first->solution() << endl;
cout << "p10, 5: " << second->solution() << endl;
delete first;
delete second;
first = new P9(12, 8);
second = new P9(12, 6);
cout << "p9, 12, 8: " << first->solution() << endl;
cout << "p9, 12, 6: " << second->solution() << endl;
delete first;
delete second;
first = new P92(12, 8);
second = new P92(12, 6);
cout << "p92, 12, 8: " << first->solution() << endl;
cout << "p92, 12, 6: " << second->solution() << endl;
delete first;
delete second;
}
여기서 P 클래스를 기준으로 P8, P9, P10 클래스를 묶고 있다. 그리고 new를 사용해서 동적으로 사용하고자 한다. 근데 자식 클래스에서 오버라이드된 함수를 사용하고자 한다. => 그래서 virtual int solution()
을 사용한 것이고, P 클래스에서는 P 클래스를 상속한 자식 클래스에게 오버라이드에 대한 의무를 반드시 주게하기 위해서 virtual int solution() = 0
을 한 것이다.
한 가지 더 봐주어야 하는데 delete 부분인데, 모든 클래스에 대해서 소멸자를 정의해주지 않았으므로 기본 소멸자를 사용하게 된다. 근데 내가 들었던 의문점은 virtual 함수를 사용했으므로 소멸자도 virtual을 사용해서 해주어야 되는거 아닌가라는 의문이 들었다.
virtual 함수를 사용한 클래스를 동적으로 할당받게 된다면 소멸할 때도, 자식 클래스의 멤버 변수, 함수를 소멸해야되므로 소멸자에도 virtual을 붙혀주어야 했다. 하지만 반드시 그럴 필요는 없다. 왜? 여기서 각 클래스 안의 멤버 변수에 포인터를 사용한 것이 없기 때문이다. 포인터를 사용하지 않았다는 것은 결국 스택 영역에 들어가게 되고, 그냥 자동으로 없어지게 되기 때문에 이 문제에서도 마찬가지로 살짝 불안정하지만 기본 소멸자를 사용해서 delete를 하는 것은 문제될 것이 없다.
template
#include <iostream>
using namespace std;
template <class T, int N>
class mysequence
{
T memblock[N];
public:
void setmember(int x, T value);
T getmember(int x);
};
template <class T, int N>
void mysequence<T, N>::setmember(int x, T value)
{
memblock[x] = value;
}
template <class T, int N>
T mysequence<T, N>::getmember(int x)
{
return memblock[x];
}
int main()
{
mysequence<int, 5> myints;
mysequence<double, 5> myfloats;
myints.setmember(0, 100);
myfloats.setmember(3, 3.1416);
cout << myints.getmember(0) << '\n';
cout << myfloats.getmember(3) << endl;
return 0;
}
댓글남기기