1월 25일 강의 정리
연산자 + 를 오버로딩으로 재정의하는 과정을 배웠다.
이어서 사칙연산과 복합대입, 논리 연산등의 응용을 해보자.
벡터를 가진 두 객체의 각 항을 더해서 +를 재정의 했었다.
같은 방식으로 뺄셈을 재정의하고,
실수로 이루어진 스칼라 값을 받아 각 항에 곱하고 나누는 곱셈나눗셈도 재정의해보자.
Vector2 Vector2::operator-(const Vector2& rhs)
{
return Vector2(x - rhs.x, y - rhs.y);
}
Vector2 Vector2::operator*(float scalar)
{
return Vector2(x * scalar, y * scalar);
}
Vector2 Vector2::operator/(float scalar)
{
return Vector2(x / scalar, y / scalar);
}
+의 응용이니 굉장히 간단하다. 참고로 x,y 에는 사실 this->x 혹은 (*this).x 가 생략되어 있다. 자기 자신의 객체를 가리키는 이 행위는 이후에 굉장히 중요하게 쓰인다.
스칼라를 벡터로 나눌수 없어 나눗셈은 그대로 구현했지만, 이대로 만들면 스칼라 값이 좌변에 위치하는 곱셈에 대응하지 않는다.
이럴때는 어쩔수 없이 전역 범위에 두 매개변수를 받도록 코드를 작성한다. 이때는 x, y를 쓸수 없고 GetX, GetY를 이용해 접근해야 한다는 점을 기억한다.
Vector2 operator*(float scalar, const Vector2& rhs)
{
return Vector2(rhs.GetX() * scalar, rhs.GetY() * scalar);
}
벡터의 부호를 변경해보자. 예를들어 객체 a를 -a 라고 입력하면 각 항에 -1 이 곱해져서 나오는 형식이다.
Vector2 Vector2::operator-()
{
return Vector2(-1 * x, -1 * y);
}
매개변수 안에 아무것도 적지 않으면 된다. 소괄호 안에는 자동으로 *this 가 입력된다.
사칙연산 이외에도 다른 연산자를 재정의 할 수 있다.
이번엔 비교 연산자를 재정의해서 객체의 각 요소가 모두 같으면 true를 반환하고, 하나라도 다르면 false를 반환하도록 해보자.
bool Vector2::operator==(const Vector2& rhs)
{
return ((x == rhs.x) && (y == rhs.y));
}
bool Vector2::operator!=(const Vector2& rhs)
{
return ((x != rhs.x) || (y != rhs.y));
// return !(*this == rhs); 아까 만든 == 응용
}
반환값이 ture false 이니 반환형도 클래스가 아닌, bool 형이 되어야 한다.
또한 ==를 구현했다면, !(*this == rhs);처럼 바로 응용해서 !=도 구현할 수 있다.
+= 과 같은 복합 대입 연산자는 대입을 필요로 하기 때문에 반환값이 약간 다르다. 자기 자신을 리턴해야 하므로 반환형 뒤에 &가 붙고, 미리 만들어둔 사칙연산을 이용하면 된다.
Vector& Vector::operator+=(const Vector rhs)
{
*this = *this + rhs;
return *this;
}
제일 어렵고 이해하기 힘든 부분이다.
<< >> 같은 연산자는 원래는 비트이동 연산자이지만 c++에서 cout cin과 함께 입출력 연산자로 사용이 된다. 이를 재정의해서 객체 이름만 적어도 요소가 전부 출력되게 만들어보자.
std::ostream& operator<<(std::ostream& lhs, const Vector& rhs)
{
return lhs << "(" << rhs.GetX() << ", " << rhs.GetY() << ")";
}
std::istream& operator>>(std::istream& lhs, Vector& rhs)
{
return lhs >> rhs.x >> rhs.y;
}
두 함수 다 전역 범위에 작성해야 한다.
각 연산자가 포함된 ostream, istream을 반환형으로 사용한다. 값이 아닌 자기 자신의 자료형이 다시 반환되어야 하기 때문에 &를 반환형과 매개변수에 둘다 붙여준다. 그후 ( x ,y ) 의 구조가 출력되도록 하면 된다.
입력은 훨씬 쉽게 구현할 수 있지만 매개변수 값을 변경해야 하기 때문에 const 를 사용할 수 없다. 또한 x와 y값에 접근할 수 없기 때문에 Vector2 클래스 내부에 >> 재정의 선언을 한번 더 해준 뒤 static 처럼 friend 라는 한정자를 붙여주면 클래스 내부의 값처럼 private 변수에 접근할 수 있다.
정적 할당은 컴파일시에 메모리 영역을 차지하지만 동적 할당은 프로그래머가 명시할 때 메모리를 할당받고 명시할때 해제된다. 따라서 컴파일시에는 이게 어느정도 문제가 될 지 모르며, 명시해주지 않으면 프로그램을 종료할때까지 메모리를 잡아먹고 있다.
따라서 프로그래밍에서 가장 중요한 것은 메모리 관리이다.
동적 할당을 했을 경우, 반드시 사용이 종료되면 해제를 해줘야 하며, 해제한 메모리도 또 해제하려 하면 에러가 나기 때문에 반드시 한번만 해제해야 한다.
또한 이미 해제한 메모리에 포인터 접근을 할때도 에러가 나기 때문에 유효한 타이밍에만 접근해야 한다. 이 두가지를 잘 하지 못하면 메모리 누수가 나고 최적화가 안됐니 cpu 과열이니 하며 유저들에게 욕을 먹는것이다.
동적 할당 연산자로는 new 와 delete 가 있다.
new 는 동적 할당을 선언하는 연산자이며 delete는 해제하는 연산자이다.
사용법은 데이터형처럼 new int() , delete ptr 같은 방식으로 쓰면 된다.
int number;
int* ptr = new int();
ptr = &number;
delete ptr;
간단하게 만들어보았다. new 연산자로 할당한 변수는 포인터 변수이며, 다른 값을 넣을수도 있다. 하지만 저렇게 number의 주소값을 넣어버리면 기존에 int값을 가지고 있던 주소값의 메모리에는 다시 접근할 방법이 없다. 종료 전까지 쓰레기 값이 메모리를 잡아먹는것이다.
또한 해제 이후 *ptr 명령을 하면 프로그램이 죽는다.
new 데이터형(생성자에 넘길 인자);
new는 결과값의 주소를 반환한다. int면 int포인터, char 은 char 포인터인 식
new int(10); 자체만 선언하는것도 메모리 누수이다. 만들어지긴 하지만 접근할 방법이 없기 때문에 int* ptr = new int(10); 처럼 주소에 접근할 방법을 줘야 한다.
반드시 몸에 배어야 할 습관 세가지를 배웠다.
1. 시작 전 널포인터로 초기화
2. 동적할당 new 를 사용하면 함수 종료부분 이전에 delete를 작성해놓고 함수를 작성하자
ㄴ delete 를 까먹기보다 처음 할당할때부터 만들어 놓아야 좋다.
3. 포인터 자체에도 널포인터를 넣어놓자
ㄴ 해제하더라도 주소값 자체는 남아있기 때문
int* ptr = nullptr;
ptr = new int(10);
if (!ptr)
{
std::cout << *ptr << std::endl;
} // 널체크 습관
delete ptr;
ptr = nullptr;
======
널 체크도 습관적으로 해주면 좋다.
int& ref = *ptr;
std::cout << ptr << std::endl;
std::cout << &ref << std::endl;
delete ptr;
ptr = nullptr;
std::cout << ref << std::endl;
이렇게 사용하고 지운 변수인데 ref 값을 또 읽어버리면 프로그램이 죽는다.
죽으면 고칠수 있어 다행이지만 쓰레기 값을 읽어버리면 며칠을 밤새서 찾아야 하는 버그가 완성되니 유의하자.