C++과 관련된 시험에서 꼭 빠지지 않고 등장하는 것이 이 OOP 문제이다.
OOP는 Class를 하나 만들어놓고, 그에 파생되는 객체를 가지고 노는 Programming이다.
이번에 해 볼 실습은, 2D Vector Class를 만들고, 해당 Class에 각 종 Operator를 Overroad 해본 뒤, OOP를 써서 Class 구현이 잘 되었는지 테스트 해보고,
2D Vector를 상속받은 Derived Class 3D Vector를 만들어 Class의 Inherit, Method의 Override를 잘 구현했는지 Test 해보겠다.
문제 풀이를 위한 Skeleton code는 아래와 같다.
#include <iostream>
#include <cmath>
using namespace std;
// Base class for 2D vectors
class Vector2D {
/*
WRITE YOUR CODE HERE
*/
};
// Derived class for 3D vectors
class Vector3D : public Vector2D {
/*
WRITE YOUR CODE HERE
*/
};
말 그대로 훵하다. 코드파일은 .hpp 타입의 header file이고, 해당 코드에 대한 test는 .cpp type의 파일을 동일한 폴더에 따로 만들어 준 뒤 #include를 통해 header file을 포함시켜 int main() 에서 test를 진행한다.
test code는 아래와 같이 만들었다.
// EX) Test.cpp라고 따로 c++파일을 headerfile과 같은 위치에 만들어준다.
#include "QE_prob3.hpp" // header file 포함시키기
#include <iostream>
int main() {
// **Test Vector2D**
Vector2D vec2D1(3, 4);
Vector2D vec2D2(1, 2);
// Test +
Vector2D vec2D3 = vec2D1 + vec2D2;
std::cout << "vec2D1 + vec2D2: ";
vec2D3.print(); // Expected: (4, 6)
// Test -
Vector2D vec2D4 = vec2D1 - vec2D2;
std::cout << "vec2D1 - vec2D2: ";
vec2D4.print(); // Expected: (2, 2)
// Test *
Vector2D vec2D5 = vec2D1 * 2; // Scalar multiplication
std::cout << "vec2D1 * 2: ";
vec2D5.print(); // Expected: (6, 8)
// Test *
Vector2D vec2D5_1 = vec2D1 * vec2D2; // Scalar multiplication
std::cout << "vec2D1 * vec2D2: ";
vec2D5_1.print(); // Expected: (3, 8)
// Test ==
Vector2D vec2D6(3, 4);
if (vec2D1 == vec2D6) {
std::cout << "vec2D1 is equal to vec2D6" << std::endl;
} else {
std::cout << "vec2D1 is not equal to vec2D6" << std::endl;
}
// Test magnitude
std::cout << "Magnitude of vec2D1: " << vec2D1.magnitude() << std::endl; // Expected: 5
// // Test normalize -> 포인터 버전
// Vector2D* vec2D7 = vec2D1.normalize();
// std::cout << "Normalized vec2D1: ";
// vec2D7->print(); // Expected: (0.6, 0.8)
// Test normalize -> 일반버전
Vector2D* vec2D7 = vec2D1.normalize();
std::cout << "Normalized vec2D1: ";
vec2D7->print(); // Expected: (0.6, 0.8)
// **Test Vector3D**
Vector3D vec3D1(3, 4, 5);
Vector3D vec3D2(1, 2, 3);
// Test +
Vector3D vec3D3 = vec3D1 + vec3D2;
std::cout << "vec3D1 + vec3D2: ";
vec3D3.print(); // Expected: (4, 6, 8)
// Test -
Vector3D vec3D4 = vec3D1 - vec3D2;
std::cout << "vec3D1 - vec3D2: ";
vec3D4.print(); // Expected: (2, 2, 2)
// Test *
Vector3D vec3D5 = vec3D1 * 2; // Scalar multiplication
std::cout << "vec3D1 * 2: ";
vec3D5.print(); // Expected: (6, 8, 10)
// Test ==
Vector3D vec3D6(3, 4, 51);
if (vec3D1 == vec3D6) {
std::cout << "vec3D1 is equal to vec3D6" << std::endl;
} else {
std::cout << "vec3D1 is not equal to vec3D6" << std::endl;
}
// Test magnitude
std::cout << "Magnitude of vec3D1: " << vec3D1.magnitude() << std::endl; // Expected: sqrt(50)
// Test normalize
Vector3D* vec3D7 = vec3D1.normalize();
std::cout << "Normalized vec3D1: ";
vec3D7->print(); // Expected: normalized vector
std::cout << "All tests completed!" << std::endl;
return 0;
}
보다시피 문제 Description은 간단하다.
Vector 끼리 +, -, *, == 를 파악하는 operator를 만들고 Vector의 Magnitude, Normalized Vector를 반환하는 Method를 만들면 된다.
완성한 코드와 test case에 대한 답안을 미리 제시하고,
코드를 짜면서 알게된 내용을 아래 후술하겠다.
/* OOP + Operator, Vector의 합,차,곱 등을 구현 */
#ifndef QE_PROB3_H
#define QE_PROB3_H
#include <iostream>
#include <cmath>
using namespace std;
// Base class for 2D vectors
class Vector2D {
/*WRITE YOUR CODE HERE*/
protected:
double x;
double y;
public:
// double x;
// double y;
// 초기화
Vector2D(double x = 0.0,double y = 0.0):x(x),y(y){}
void setX(double x_val) { x = x_val; }
double getX(){ return x; }
void setY(double y_val) { y = y_val; }
double getY(){ return y; }
Vector2D operator+(const Vector2D &other) const {
return Vector2D(x + other.x , y + other.y);
}
Vector2D operator-(const Vector2D &other) const {
return Vector2D(x - other.x , y - other.y);
}
Vector2D operator*(const Vector2D &other) const {
return Vector2D(x * other.x , y * other.y);
}
Vector2D operator*(const double num) const {
return Vector2D(x * num , y * num);
}
bool operator==(const Vector2D &other) const {
return x==other.x && y==other.y;
}
friend std::ostream &operator<<(std::ostream &out,const Vector2D &vec){
out << "Vector2D(" << vec.x << ", " << vec.y << ")";
return out;
}
void print() const {
cout << *this << endl;
}
// void print() const {
// cout<<"Vector2D(" << x << ", " << y << ")"<<endl;
// } 이렇게 해도 무방
virtual float magnitude() const;
virtual Vector2D* normalize() const; // 포인터버전
// virtual Vector2D normalize() const; // 일반버전
};
float Vector2D::magnitude() const{
return sqrt(pow(x,2)+pow(y,2));
}
Vector2D* Vector2D::normalize() const{ // 포인터버전
if(this->magnitude()==0) return const_cast<Vector2D*>(this);
// 희한하다.. normalize가 const로 되어 있으면 const_cast 해줘야 함.
Vector2D* temp = new Vector2D(x/(this->magnitude()),y/(this->magnitude()));
return temp;
}
// Vector2D Vector2D::normalize() const{
// if(this->magnitude()==0) return *this;
// return Vector2D(x/this->magnitude(),y/this->magnitude());
// }
// Derived class for 3D vectors
class Vector3D : public Vector2D {
/*WRITE YOUR CODE HERE */
private: double z;
public:
Vector3D(double x_val = 0.0 ,double y_val = 0.0, double z_val = 0.0):Vector2D(x_val,y_val),z(z_val){}
void setZ(double input_z){z=input_z;}
double getZ(){return z;}
Vector3D operator+(const Vector3D &other) const {
return Vector3D(x + other.x , y + other.y, z + other.z);
}
Vector3D operator-(const Vector3D &other) const {
return Vector3D(x - other.x , y - other.y,z-other.z);
}
Vector3D operator*(const Vector3D &other) const {
return Vector3D(x * other.x , y * other.y, z*other.z);
}
Vector3D operator*(const double num) const {
return Vector3D(x * num , y * num,z*num);
}
bool operator==(const Vector3D &other) const {
return x==other.x && y==other.y && z==other.z;
}
friend std::ostream &operator<<(std::ostream &out,const Vector3D &vec){
out << "Vector3D(" << vec.x << ", " << vec.y << ", "<<vec.z<<")";
return out;
}
void print() const {
cout << *this << endl;
}
float magnitude() const override{
return sqrt(pow(x,2)+pow(y,2)+pow(z,2));
}
Vector3D* normalize() const override{ //override 해줘야 return 값 바꿀 수 있다
if (this->magnitude() == 0) return new Vector3D(*this);
return new Vector3D(x / this->magnitude(), y / this->magnitude(), z / this->magnitude());
}
// Pointer나 Reference type만 override를 통해 return type을 바꿀 수 있다.. 중요!
};
#endif
뭐.. 전체적으로 코드는 잘 짜졌는데, 솔직히 중간중간에 Error가 좀 나서 원인파악을 위해 GPT 도움을 받았다.
완전 No Support 공간에서 백지장에서 해당 OOP Class를 만들 수 있을만큼 내공을 쌓아야 한다.
위 코드에도 주석으로 새로이 알게된 내용을 써놨긴 했다만 다시 정리하면 아래와 같다.
1. Derived Class 는 Base Class의 Public or Protected 영역에 있는 Attribute , Method만 접근이 가능하다.
이를테면, Base class인 Vector2D class에서 x,y를 private 영역에 넣었다면 Vector3D class가 2D를 상속받아도 x,y에 접근하지 못한다. 따라서 이럴 때는 getter -> getX, getY 함수를 통해 3D 함수를 구현하면 된다.
2. Class Constructor 부분에 Initial 값을 써주는 것이 좋다.
Vector2D(double x = 0.0,double y = 0.0):x(x),y(y){}
이를테면 위의 경우 Vector2D class에 그냥 x,y를 넣으면 0.0 , 0.0 으로 초기값이 들어간다.
그래서 Vector2D temp; 라고 그냥 아무런 input 없이 변수를 선언해도 동작에는 지장이 없다.
하지만, 이런 초기값이 없으면 저 temp는 x,y에 어떤 값을 지정해야 하는지 몰라서 저런 식으론 선언이 안 된다.
3. 한 Class 안에 변수 Type에 따라 Operator를 중복으로 만들어줄 수 있다.
Vector2D operator*(const Vector2D &other) const {
return Vector2D(x * other.x , y * other.y);
}
Vector2D operator*(const double num) const {
return Vector2D(x * num , y * num);
}
위 경우, Vector2D 라는 Class 안에 * operator를 2개 선언해주었다.
Vector는 Vector끼리 곱할 수도 있고 Vector와 Scalar가 곱해질 수도 있기에 저렇게 따로 분류해 놓으면
* operator 뒤에 오는 Variable의 type에 따라 알아서 method가 취사선택되어 실행된다.
4. 출력을 위한 '<<' operator는 아래와 같이 overroad 할 수 있다. + '<<' operator에 대한 설명
operator << 를 비멤버 함수로 선언하면 friend를 써야한다.
비멤버 함수로 선언되어야 ostream , Vector2D 객체에 모두 접근할 수 있다.
class Vector2D {
public:
double x;
double y;
Vector2D(double x = 0.0, double y = 0.0) : x(x), y(y) {}
friend std::ostream &operator<<(std::ostream &out, const Vector2D &vec); // friend 선언
};
std::ostream &operator<<(std::ostream &out, const Vector2D &vec) {
out << "Vector2D(" << vec.x << ", " << vec.y << ")";
return out;
}
friend 선언 없이 print 하려면 그냥 다음과 같이 print 함수를 만들면 된다.
class Vector2D {
protected:
double x, y;
public:
Vector2D(double x_val = 0, double y_val = 0) : x(x_val), y(y_val) {}
std::ostream &print(std::ostream &out) const { // 멤버 함수
out << "Vector2D(" << x << ", " << y << ")";
return out;
}
};
std::ostream &operator<<(std::ostream &out, const Vector2D &vec) {
return vec.print(out); // 멤버 함수 호출
}
friend 선언 없이 operator << 를 overroad 하게 되면, <<는 암묵적으로 첫 번째 인자를 'this'로 받게되어 인자 받는 과정에서 error가 발생한다. 따라서 friend 없으면 그냥 비멤버 선언으로 해줘야 한다.
+++) Const로 선언된 객체는 Const만 호출이 가능하다.
5. Vitrual / Override
Base class 내의 특정 함수를 Virtual로 만들면, 그것을 Derive 하는 함수에서 Override를 통해 해당 함수의 기능을 바꿔서 쓸 수 있다.
{
//Base class 내부
virtual float magnitude() const;
}
float Vector2D::magnitude() const{
return sqrt(pow(x,2)+pow(y,2));
}
Base에선 이렇게 virtual method를 선언만 해주고, class 밖에서 Scope (Vector2D ::)를 통해 해당 함수를 마저 정의할 수 있다.
만약 해당 Virtual Method가 Derived class에서 무조건 정의되어야 하는 Method라면 Abstract Class를 만들어 주면 된다.
(Virtual + =0 )
다시, Virtual로 돌아와서, Derived Class에선 해당 Virtual Method를 Override해서 변주를 준 후에 사용한다.
class Vector3D : public Vector2D {
float magnitude() const override{
return sqrt(pow(x,2)+pow(y,2)+pow(z,2));
}
}
6. C++의 공변적 반환 타입(Covariant return type) 규정을 따르려면 반환 타입이 포인터 또는 참조 여야 한다.
지금 위 5번의 예시에선 magnitude라는 method가 Vector 2D / 3D 에서 모두 float을 return 하므로 문제가 없다. 하지만 Method의 return type이 derived에서 바뀌게 되면 문제가 발생한다.
이 때의 return type은 일반 변수이면 Covariance return type 규칙에 위반되기 때문에 안 된다. 무조건 Return Type은 Pointer 나 Reference 타입이어야만 허용된다.
class Vector2D {
virtual Vector2D* normalize() const; // return type이 pointer나 Reference 여야 한다.
}
Vector2D* Vector2D::normalize() const{ // 포인터버전
if(this->magnitude()==0) return const_cast<Vector2D*>(this);
// 희한하다.. normalize가 const로 되어 있으면 const_cast 해줘야 함.
Vector2D* temp = new Vector2D(x/(this->magnitude()),y/(this->magnitude()));
return temp;
}
class Vector3D : public Vector2D {
Vector3D* normalize() const override{ //override 해줘
if (this->magnitude() == 0) return new Vector3D(*this);
return new Vector3D(x / this->magnitude(), y / this->magnitude(), z / this->magnitude());
}
}
그리고 이 때 magnitude가 0인 경우에 처럼 this를 그대로 return해 줄 때에는 const_cast<Vector2D*> 처럼 type을 casting해준다.
Reference로 반환시켜주는 Case는 아래와 같다.
class Vector2D {
protected:
double x, y;
public:
Vector2D(double x = 0.0, double y = 0.0) : x(x), y(y) {}
virtual float magnitude() const {
return sqrt(x * x + y * y);
}
virtual Vector2D& normalize() const {
static Vector2D temp; // 반환할 객체를 static으로 선언
float mag = magnitude();
if (mag == 0) {
temp = *this; // 크기가 0일 경우, 자기 자신 반환
} else {
temp = Vector2D(x / mag, y / mag);
}
return temp;
}
};
class Vector3D : public Vector2D {
private:
double z;
public:
Vector3D(double x = 0.0, double y = 0.0, double z = 0.0) : Vector2D(x, y), z(z) {}
float magnitude() const override {
return sqrt(x * x + y * y + z * z);
}
Vector3D& normalize() const override {
static Vector3D temp; // 반환할 객체를 static으로 선언
float mag = magnitude();
if (mag == 0) {
temp = *this; // 크기가 0일 경우, 자기 자신 반환
} else {
temp = Vector3D(x / mag, y / mag, z / mag);
}
return temp;
}
};
'SW 만학도 > C++ & Algorithm' 카테고리의 다른 글
Heap Implementation [1] (0) | 2024.12.26 |
---|---|
Review 11 - Dynamic Programming (0) | 2024.08.03 |
Review 10 - Single-Source-Shortest Path in C++ (0) | 2024.08.02 |
Review 9 - MST(Minimum Spanning Trees) in C++ (8) | 2024.07.24 |
[Algorithm] Review 8 - Priority Queues and Heaps in C++ (3) | 2024.07.22 |