서론
평가를 받고 나면 바로바로 글을 남겨야 글이 술술 나오는데, 평가를 받고 며칠 후에 글을 쓰려니 내가... 뭐라고 했더라...라는 생각이 들면서 글이 제대로 나오질 않는 것 같다.
본가에 내려가는 버스가 마지막 평가 끝나고 한 시간 뒤여서 급하게 평가를 받았고, 집에 내려가서도 공부를 안 하다 보니 내 과제이지만 뭔가 생소하다. 긍정적으로 생각해보면 에빙하우스의 망각곡선처럼 더 까먹을 때쯤 정리를 다시 하니 복습이 더 잘되는 것 같기도 하다.
코드를 다 완성하고 나면 서브젝트와 코드 내용을 노션에 정리한 다음, 1~2일 후에 평가를 받고, 거기에서 3~4일 후에 글을 쓰니까 사실 난 의도적으로 복습을 하고 있는 게 아닐까?
ex00
부동소수점에 대한 칼럼이 나오고, c++에 고정소수점이 없으니까 그걸 만들어보자는 내용이 있다. 칼럼은 그럭저럭 재미있는데, 다 이해하면 어디 가서 '컴퓨터가... 왜 소수를 완벽하게 표현할 수 없는지 알아...?' 이렇게 아는 체하기 좋다.
이 과제는 Orthodox Canonical Form을 이용하여 고정 소수점을 만드는 과제이다. 정확한 이름은 OCCF(orthodox canonical class form)인데 밑에서 다 구현하겠지만, 그전에 https://www.francescmm.com/orthodox-canonical-class-form/ 를 보면 좋다.
클래스의 private 멤버에는 고정 소수점 값을 저장할 int 하나와 소수의 비트 수를 저장할 상수 정수 하나를 만들고, 그 상수에는 8을 저장한다.
public 멤버에는 기본 성생자(value 를 0으로 함), 복사 생성자, 할당 연산자 오버로드, 소멸자, 그리고 get set 함수를 만드는데, 고정 소수점 값의 raw값을 반환하거나 set 한다.
메인 문의 내용과 그 출력 내용은 아래와 같다.
#include <iostream>
int main( void ) {
Fixed a;
Fixed b( a );
Fixed c;
c = b;
std::cout << a.getRawBits() << std::endl;
std::cout << b.getRawBits() << std::endl;
std::cout << c.getRawBits() << std::endl;
return 0;
}
$> ./a.out
Default constructor called
Copy constructor called
Copy assignment operator called // <-- This line may be missing depending on your implementation
getRawBits member function called
Default constructor called
Copy assignment operator called
getRawBits member function called
getRawBits member function called
0
getRawBits member function called
0
getRawBits member function called
0
Destructor called
Destructor called
Destructor called
$>
#ifndef FIXED_HPP
# define FIXED_HPP
# include <iostream>
class Fixed {
private:
int value;
const static int bits = 8;
public:
Fixed(void);
~Fixed(void);
Fixed(const Fixed& obj);
Fixed& operator=(const Fixed& obj);
int getRawBits(void) const;
void setRawBits(int const raw);
};
#endif
#include "Fixed.hpp"
Fixed::Fixed(void)
{
std::cout << "Default constructor called" << std::endl;
this->value = 0;
}
Fixed::Fixed(const Fixed& obj)
{
std::cout << "Copy constructor called" << std::endl;
this->value = obj.getRawBits();
}
Fixed& Fixed::operator=(const Fixed& obj)
{
std::cout << "Copy assignment operator called" << std::endl;
if (this != &obj)
{
this->value = obj.getRawBits();
}
return (*this);
}
Fixed::~Fixed(void)
{
std::cout << "Destructor called" << std::endl;
}
int Fixed::getRawBits(void) const
{
std::cout << "getRawBits member function called" << std::endl;
return (this->value);
}
void Fixed::setRawBits(int const raw)
{
this->value = raw;
}
할당 연산자와 복사 연산자 오버 로딩을 제외하면 어렵지 않다.
할당 연산자는 a = b; 같은 형태로 b를 인자로 받아 a에 대한 참조를 반환하도록 만들고, b의 값이 a에 담기도록 함수 내부를 구성하면 된다. 사실은 개발자가 직접 만들지 않으면 컴파일러가 알아서 만들지만, 개발자가 의도한 대로 동작하지 않을 수 있기 때문에 직접 구현하는 것이 옳은 방법이다.
복사 생성자는 같은 클래스의 레퍼런스를 인자로 받아 그 레퍼런스의 값을 이용하여 새로운 클래스를 만드는 생성자이다. 깊은 복사 - deep copy - 값 복사를 하기 때문에 원본 객체와 같으면서 독립성을 지닌 객체를 만드는 데 사용한다.
과제 출력 예시를 보면 copy나 copy assignment를 할 때 getRawBits 함수를 사용하여 value를 가져오는 걸 알 수 있다. 따라서 해당 함수를 사용하여 구현하였다.
인자의 const는 인자의 값을 변경하지 않음을 보장하기 위해, 함수 getRawBits 끝에 const는 이 함수에서 이 클래스의 값을 변경하지 않음을 보장하기 위해 사용한다.
https://www.delftstack.com/ko/howto/cpp/assignment-operator-overloading-in-cpp/
http://www.tcpschool.com/cpp/cpp_conDestructor_copyConstructor
스니펫 만들기
같은 형태로 계속 같은 작업을 해야 한다면 당연히 자동화해야 하지 않을까? 같은 형태의 클래스를 만들 일이 많아지면서 스니펫에 대한 관심이 생겼는데, 이리저리 고민하다가 최종적으로는 ulee님의 스니펫을 가져와서 입맛에 맞게 조금 고쳤다.
"Orthodox Canonical Class Form": {
"prefix": "clas",
"body": [
"#ifndef ${TM_FILENAME/^([^\\.]*)\\..*$/${1:/upcase}/}_${TM_FILENAME/^.*\\.([^\\.]*)$/${1:/upcase}/}",
"# define ${TM_FILENAME/^([^\\.]*)\\..*$/${1:/upcase}/}_${TM_FILENAME/^.*\\.([^\\.]*)$/${1:/upcase}/}\n",
"# include <iostream>",
"$0",
"",
"class ${TM_FILENAME_BASE} {",
" private:",
" \t${1:/* data */}",
" public:",
" \t${TM_FILENAME_BASE}(${2:/* args */});",
" \t${TM_FILENAME_BASE}(const ${TM_FILENAME_BASE}& obj);",
" \t~${TM_FILENAME_BASE}();",
" \t${TM_FILENAME_BASE}& operator=(const ${TM_FILENAME_BASE}& obj);",
"};",
"\n#endif\n",
"${TM_FILENAME_BASE}::${TM_FILENAME_BASE}(${2:/* args */}) {}",
"${TM_FILENAME_BASE}::${TM_FILENAME_BASE}(const ${TM_FILENAME_BASE}& obj) {}",
"${TM_FILENAME_BASE}::~${TM_FILENAME_BASE}() {}",
"${TM_FILENAME_BASE}& ${TM_FILENAME_BASE}::operator=(const ${TM_FILENAME_BASE}& obj) {}",
""
],
"description": "Orthodox Canonical Class Form"
}
스니펫에 대한 자세한 설명은
https://velog.io/@humblego42/VSCode에서-Snippet-활용하기
위 스니펫의 원본은
https://42born2code.slack.com/archives/CU6MU5TB7/p1641732468209900
ex01
00에서 구현한 건 좋은 시작이었지만, 늘 0.0만 사용하게 되는 아무런 쓸모가 없는 클래스이다. 따라서 다음의 생성자들을 만들어서 public 멤버 함수로 추가해야 한다.
- const int 인자를 받아서 고정 소수점 값으로 변환하는 함수
- const float 인자를 받아서 고정 소수점 값으로 변환하는 함수
- 고정 소수점 값을 float로 변환하여 반환하는 float toFloat(void) const 함수
- 고정 소수점 값을 int로 변환하여 반환하는 int toInt(void) const 함수
- << 연산자에 대한 오버로드. 출력 스트림에 고정 소수점 값을 float로 변환하여 넘김
힌트로 <cmath>의 roundf 가 허용 함수이다.
#include <iostream>
int main( void ) {
Fixed a;
Fixed const b( 10 );
Fixed const c( 42.42f );
Fixed const d( b );
a = Fixed( 1234.4321f );
std::cout << "a is " << a << std::endl;
std::cout << "b is " << b << std::endl;
std::cout << "c is " << c << std::endl;
std::cout << "d is " << d << std::endl;
std::cout << "a is " << a.toInt() << " as integer" << std::endl;
std::cout << "b is " << b.toInt() << " as integer" << std::endl;
std::cout << "c is " << c.toInt() << " as integer" << std::endl;
std::cout << "d is " << d.toInt() << " as integer" << std::endl;
return 0;
}
$> ./a.out
Default constructor called
Int constructor called
Float constructor called
Copy constructor called
Copy assignment operator called
Float constructor called
Copy assignment operator called
Destructor called
a is 1234.43
b is 10
c is 42.4219
d is 10
a is 1234 as integer
b is 10 as integer
c is 42 as integer
d is 10 as integer
Destructor called
Destructor called
Destructor called
Destructor called
main문을 가져온 다음 위에서부터 에러를 해결하면서 과제를 진행하면 조금 더 직관적이다.
Fixed::Fixed(int num)
{
std::cout << "Int constructor called" << std::endl;
this->value = num << this->bits;
}
Fixed::Fixed(const float num)
{
std::cout << "Float constructor called" << std::endl;
this->value = roundf(num * (1 << this->bits));
}
가장 먼저 생성자를 만들어준다. int 생성자에서는 정해진 비트수만큼 비트 쉬프트 연산을 해서 저장한다.
float 생성자에서는 float 자료형은 비트 연산이 불가능하기 때문에 num * (1 << bits)로 우회하여 표현한다. roundf를 사용하여 반올림한 값을 저장하게 되는데, main문을 보면 42.42f를 저장하도록 되어있다. 42.42 * 256을 저장할 때 10859.52를 roundf하지 않고 저장하면 소수점 밑 값이 그냥 사라지므로 조금이라도 더 근접하게 저장할 수 있도록 반올림해서 저장한다.
std::ostream& operator <<(std::ostream &out, const Fixed &fixed)
{
out << fixed.toFloat();
return (out);
}
그다음으로는 cout << 연산을 오버 로딩해준다. 다른 클래스의 동작을 오버 로딩하는 것이기 때문에 (멤버 함수가 아니라) 따로 선언해준다.
cout의 오버 로딩은 위와 같은 형태로 하면 된다. 당연히 에러를 뱉을 텐데, toFloat가 없기 때문이다.
float Fixed::toFloat(void) const
{
return ((float)this->value / (1 << this->bits));
}
int Fixed::toInt(void) const
{
return (this->value >> this->bits);
}
toFloat와 toInt를 구현한다.
float는 위에서 구현한 방법의 역순으로, value를 float으로 캐스팅한 다음, 이번에는 비트 연산한 걸로 나눠서 구현한다. int도 마찬가지로 비트 연산을 반대로 하면 된다.
ex00에서 구현한 내용 중 getRawBits의 cout을 제거해주면 서브젝트의 예제와 동일하게 출력이 된다.
https://blog.naver.com/kks227/60205596757
ex02
다음 연산자들을 구현하라
- 비교 연산자 >, <, >=, <=, ==, !=
- 산술 연산자 +, -, *, /
- 증감 연산자 f++, ++f, f—, —f
min 함수
- 두 개의 참조값을 가져와 가장 작은 값의 참조를 반환하는 함수
- 두 개의 상수 참조값을 가져와 가장 작은 값의 참조를 반환하는 함수
max 함수
- 두 개의 참조값을 가져와 가장 큰 값의 참조를 반환하는 함수
- 두 개의 상수 참조값을 가져와 가장 큰 값의 참조를 반환하는 함수
int main( void ) {
Fixed a;
Fixed const b( Fixed( 5.05f ) * Fixed( 2 ) );
std::cout << a << std::endl;
std::cout << ++a << std::endl;
std::cout << a << std::endl;
std::cout << a++ << std::endl;
std::cout << a << std::endl;
std::cout << b << std::endl;
std::cout << Fixed::max( a, b ) << std::endl;
return 0;
}
$> ./a.out
0
0.00390625
0.00390625
0.00390625
0.0078125
10.1016
10.1016
$>
bool operator>(Fixed const &ref) const;
bool operator<(Fixed const &ref) const;
bool operator>=(Fixed const &ref) const;
bool operator<=(Fixed const &ref) const;
bool operator==(Fixed const &ref) const;
bool operator!=(Fixed const &ref) const;
bool Fixed::operator>(Fixed const &ref) const
{
return (this->getRawBits() > ref.getRawBits());
}
bool Fixed::operator<(Fixed const &ref) const
{
return (this->getRawBits() < ref.getRawBits());
}
bool Fixed::operator>=(Fixed const &ref) const
{
return (this->getRawBits() >= ref.getRawBits());
}
bool Fixed::operator<=(Fixed const &ref) const
{
return (this->getRawBits() <= ref.getRawBits());
}
bool Fixed::operator==(Fixed const &ref) const
{
return (this->getRawBits() == ref.getRawBits());
}
bool Fixed::operator!=(Fixed const &ref) const
{
return (this->getRawBits() != ref.getRawBits());
}
비교 연산자 오버 로딩은 간단하다. bool 형태로 반환하도록 만들고, return에서 getRawBits의 크기를 비교하도록 만들면 된다.
Fixed operator+(Fixed const &ref) const;
Fixed operator-(Fixed const &ref) const;
Fixed operator*(Fixed const &ref) const;
Fixed operator/(Fixed const &ref) const;
Fixed Fixed::operator+(Fixed const &ref) const
{
Fixed ret(this->toFloat() + ref.toFloat());
return (ret);
}
Fixed Fixed::operator-(Fixed const &ref) const
{
Fixed ret(this->toFloat() - ref.toFloat());
return (ret);
}
Fixed Fixed::operator*(Fixed const &ref) const
{
Fixed ret(this->toFloat() * ref.toFloat());
return (ret);
}
Fixed Fixed::operator/(Fixed const &ref) const
{
Fixed ret(this->toFloat() / ref.toFloat());
return (ret);
}
산술 연산자도 기존에 만든 float 값을 인자로 받는 생성자를 이용하여 쉽게 구현할 수 있다.
Fixed &operator++(void);
const Fixed operator++(int);
Fixed &operator--(void);
const Fixed operator--(int);
Fixed& Fixed::operator++(void)
{
this->value++;
return (*this);
}
const Fixed Fixed::operator++(int)
{
const Fixed ret(*this);
this->value++;
return (ret);
}
Fixed& Fixed::operator--(void)
{
this->value--;
return (*this);
}
const Fixed Fixed::operator--(int)
{
const Fixed ret(*this);
this->value--;
return (ret);
}
전위 연산자와 후위 연산자를 구별하기 위해서 인자에 void와 int로 나눠서 써준다. 특별한 이유가 있는 건 아니고 컴파일러에게 dummy로 구별해서 전위인지 후위인지를 알려주는 역할을 한다. 인자의 반환 값으로는 컴파일러가 해당 함수가 전위인지 후위인지를 구별할 수 없으므로 생긴 하나의 규칙이다.
전위 연산자를 먼저 보면 this→value를 증감시킨 후에 *this의 레퍼런스를 반환하고 있다. 이게 레퍼런스에 익숙하지 않은 상태에서 처음 보면 엄청 헷갈릴 텐데, C라고 생각하고 다시 보자. this 구조체의 변수를 가져올 때 '.' 이 아닌 '→' 를 사용하기 때문에 this는 포인터라는 걸 알 수 있다. 따라서 포인터의 원래 값을 반환하기 위해 * 를 단항 연산자로 사용하여 값을 반환하도록 하고, 그것의 레퍼런스 화는 자동으로 이루어진다. 만약 *this가 아닌 그냥 this를 반환하려 한다면, 임시 객체의 레퍼런스를 만들려고 하기 때문에 컴파일 에러가 나게 된다.
후위 연산자에는 const가 붙어있다. 후위 연산자의 반환 값은 임시로 만들어진 객체이므로 해당 값에 다시 증감 연산을 했을 때 임시 객체의 값이 증감하게 되는 문제가 있다. 그걸 막기 위해 const를 붙여준다.
예를 들어 아래 코드는 컴파일 에러를 뱉는다
int a = 1;
(a++)++;
std::cout << a << std::endl;
a++의 반환값은 2이며 2++는 상수를(정확히는 rvalue를) 변환할 수 없다는 c++의 원칙에 위배된다.
만약 내가 만든 클래스에서 후위 연산에 const를 붙여주지 않으면 이러한 연산을 막지 못해서 문제가 생기기 때문에, (임시 객체가 상수는 아니니) 컴파일 에러를 뱉진 않지만 의도하지 않은 동작인 임시 객체의 증감을 막기 위하여 함수에 const를 붙여 상수로 만들어야 한다.
static Fixed &min(Fixed &ref1, Fixed &ref2);
static const Fixed &min(Fixed const &ref1, Fixed const &ref2);
static Fixed &max(Fixed &ref1, Fixed &ref2);
static const Fixed &max(Fixed const &ref1, Fixed const &ref2);
Fixed& Fixed::min(Fixed &ref1, Fixed &ref2)
{
if (ref1 <= ref2)
return ref1;
else
return ref2;
}
const Fixed& Fixed::min(Fixed const &ref1, Fixed const &ref2)
{
if (ref1 <= ref2)
return ref1;
else
return ref2;
}
Fixed& Fixed::max(Fixed &ref1, Fixed &ref2)
{
if (ref1 >= ref2)
return ref1;
else
return ref2;
}
const Fixed& Fixed::max(Fixed const &ref1, Fixed const &ref2)
{
if (ref1 >= ref2)
return ref1;
else
return ref2;
}
받은 레퍼런스가 상수이니 반환도 상수로 하는 걸 제외하면 다 기존에 구현한 내용을 사용하거나 비슷하게 구현하는 내용이기 때문에 크게 어렵지 않다. 평가 가면 ref1.getRawBits() > … 이런 식으로 구현한 코드들을 가끔 만나는데, 이미 > 연산자에서 그 부분이 들어가 있기 때문에 그렇게까지는 안 해도 된다고 생각한다.
https://husk321.tistory.com/255
https://sexycoder.tistory.com/11
https://effort4137.tistory.com/entry/Lvalue-Rvalue
https://hwan-shell.tistory.com/38
ex03
좌표를 의미하는 Point 클래스를 하나 만든다.
private 멤버
- x, y 그리고 필요한 무언가
public 멤버
- x, y를 0으로 초기화하는 기본 생성자
- 두 개의 상수 float을 가져와서 x, y에 초기화하는 생성자
- 복사 생성자
- 할당 생성자 오버로드
- 소멸자
- 그 외 필요한 무언가
아래 함수를 만든다
bool bsp( Point const a, Point const b, Point const c, Point const point);
포인트 a, b, c : 삼각형의 꼭짓점
포인트 point : 체크해야 할 포인트
반환 값 : point가 abc 꼭짓점으로 이루어진 삼각 형안에 있다면 true, 아니라면 false.
#ifndef POINT_HPP
# define POINT_HPP
# include <iostream>
# include "Fixed.hpp"
class Point {
private:
Fixed const x;
Fixed const y;
public:
Point(void);
Point(float const x, float const y);
Point(const Point& obj);
Point& operator=(const Point& obj);
~Point(void);
Fixed getX(void) const;
Fixed getY(void) const;
};
bool bsp(Point const a, Point const b, Point const c, Point const point);
#endif
#include "Point.hpp"
Point::Point(void) : x(0), y(0)
{
}
Point::Point(float const x, float const y) : x(x), y(y)
{
}
Point::Point(const Point& obj) : x(obj.getX()), y(obj.getY())
{
}
Point& Point::operator=(const Point& obj)
{
if (this != &obj)
{
const_cast<Fixed&>(x) = obj.getX();
const_cast<Fixed&>(y) = obj.getY();
}
return *this;
}
Point::~Point(void)
{
}
Fixed Point::getX(void) const
{
return (x);
}
Fixed Point::getY(void) const
{
return (y);
}
클래스는 쉽게 구현했는데, 멤버 변수를 상수로 저장하라고 해놓고 할당 연산자를 구현하도록 되어있어서 잠시 혼란이 있었다.
우선 잠시 const를 지운 다음 가져오도록 캐스팅해서 구현하였는데, 아무래도 이걸 의도하진 않았을 것 같고, 과제가 뭔가 잘못된 것 같다는 생각을 지울 수가 없었다.
임의의 점이 삼각형 안에 있는지를 확인하기 위한 방법은 그 점에서 오른쪽으로 선을 그어서 삼각형의 선과 한 번만 닿는지 보면 된다. 만약 두 번 닿는다면 삼각형의 왼쪽, 닿지 않는다면 삼각형의 오른쪽에 있기 때문이다.
((B.x - A.x) * ((P.y - A.y) / (B.y - A.y)) + A.x)
((2 - 5) * (7 - 5) / (9 - 5) + 5)
3.5 가 P에서 오른쪽으로 닿는 선분 BA의 지점
삼각형에서 선분의 비율을 이용하여 선분과 닿는 지점을 계산하는 공식이다.
((B.x - A.x) * ((P.y - A.y) / (B.y - A.y)) + A.x) >= P.x
((2 - 5) * (7 - 5) / (9 - 5) + 5) > 3
3.5 > 3 이므로 P는 해당 선의 왼쪽에 존재한다.
이를 이용하여 왼쪽에 있는지를 판단하는 식을 구할 수 있다.
bool ret = false;
if (P.y > A.y != P.y > B.y)
// 둘다 true거나 flase면 flase. 둘이 엇갈리면 ture.
{
if (((B.x - A.x) * ((P.y - A.y) / (B.y - A.y)) + A.x) >= P.x)
{
if (ret)
ret = false;
else
ret = true;
}
// ret 가 false일때는 아직 선분을 만난게 아니므로 true로 바꿔주고,
// ture일때는 선분을 만난것이므로 false로 바꿔준다
return (ret);
}
단, 이 식을 사용하기 위해서는 P.y는 B.y보다는 작고 A.y보다는 커야 한다. 반대로 A.y보다는 작고 B.y보다는 커도 동일하게 적용되므로, 코드로 표현하면 이렇게 요약할 수 있다.
bool bsp(Point const a, Point const b, Point const c, Point const point)
{
bool ret = false;
if ((a.getY() > point.getY()) != (b.getY() > point.getY()))
{
if (((b.getX() - a.getX()) * ((point.getY() - a.getY()) / (b.getY() - a.getY())) + a.getX()) >= point.getX())
{
if (ret)
ret = false;
else
ret = true;
}
}
if ((a.getY() > point.getY()) != (c.getY() > point.getY()))
{
if (((c.getX() - a.getX()) * ((point.getY() - a.getY()) / (c.getY() - a.getY())) + a.getX()) >= point.getX())
{
if (ret)
ret = false;
else
ret = true;
}
}
if ((b.getY() > point.getY()) != (c.getY() > point.getY()))
{
if (((c.getX() - b.getX()) * ((point.getY() - b.getY()) / (c.getY() - b.getY())) + b.getX()) >= point.getX())
{
if (ret)
ret = false;
else
ret = true;
}
}
return (ret);
}
이제 이걸 선분 세 번에 각각 반복하도록 구성하면 된다. 삼각형의 안에 있다면 세 번 중 두 번의 if문에 들어가서 그중 한 번만 그 안의 if문까지 들어갈 것이다.
int main(void)
{
Point a(5, 5);
Point b(2, 9);
Point c(0, 5);
Point p(3, 7);
Point p2(4, 7);
std::cout << "Point a.x = " << a.getX() << " a.y = " << a.getY() << std::endl;
std::cout << "Point b.x = " << b.getX() << " b.y = " << b.getY() << std::endl;
std::cout << "Point c.x = " << c.getX() << " c.y = " << c.getY() << std::endl;
std::cout << "Point p.x = " << p.getX() << " p.y = " << p.getY() << std::endl;
std::cout << "Point p2.x = " << p2.getX() << " p2.y = " << p2.getY() << std::endl;
if (bsp(a, b, c, p))
std::cout << "p is in the triangle" << std::endl;
else
std::cout << "p is out of the triangle" << std::endl;
if (bsp(a, b, c, p2))
std::cout << "p2 is in the triangle" << std::endl;
else
std::cout << "p2 is out of the triangle" << std::endl;
return 0;
}
이렇게 하면 과제 끝.
https://velog.io/@appti/CPP-Module-02-ex03
https://www.algeomath.kr/algeomath/app/make.do
'프로그래밍' 카테고리의 다른 글
[42서울] CPP Module 04 - 다형성과 추상클래스 (0) | 2022.09.10 |
---|---|
[42서울] CPP Module 03 - 클래스 상속 (0) | 2022.08.18 |
[42서울] CPP Module 01 - 클래스와 레퍼런스 (0) | 2022.08.10 |
[42서울] CPP Module 00 - c++ 과제의 시작 (0) | 2022.07.28 |
[42서울] cub3D는 아름다워(Cub 3d è bella) (0) | 2022.07.23 |