개체지향 프로그래밍2

복사(COPY) 생성자

  • 생성자의 매개변수가 나와 같은 개체라면 복사 생성자라고 생각하면 된다. -> 같은 클래스에 속한 개체를 이용하여 새로운 개체를 초기화한다. -> 말 그대로 복사다! -> 자바 Clone 과 비슷하다~

// Vector.h
class Vector
{
public:
    Vector(const Vector& other);    // 복사 생성자
private:
    int mX;
    int mY;
}

// Vector.cpp
Vector::Vector(const Vector& other)
    : mX(other.mx)
    , mY(other.mY)
{
}

Vector a;
Vector b(a);

암시적(implicit) 복사 생성자

  • 코드에 복사 생성자가 없는 경우 컴파일러가 암시적으로 복사 생성자를 생성한다. (기본 생성자가 없는 경우도 동일하다)

  • 암시적 복사 생성자는 얕은 복사(값 복사) 를 수행한다.

  • 개체인 맴버변수는 그 개체의 복사 생성자가 호출된다.

클래스에 포인터 형 변수가 있다면?

  • 얕은 복사이기에 소멸자가 호출되며 복사 된 힙 영역을 반납하는 문제가 생겨버린다. -> 얕은 복사의 문제 -> 복사 생성자를 통해서 힙 영역을 공유하는 것은 상당히 위험하다!

사용자가 만든 복사 생성자

  • 클래스 안에서 동적으로 메모리를 할당하고 있다면!? 위와 같은 상황으로 얕은 복사가 위험할 가능성이 매우 높다!

  • 때문에, 직접 복사 생성자를 만들어서 깊은 복사(deep copy) 를 할 것! -> 포인터 변수가 가리키는 실제 데이터까지도 복사한다.

연산자 오버로딩

  • 연산자를 다시 한번 생각해 보면 함수와 같다! (임의의 INPUT 에 대해 임의의 OUTPUT 을 만들어낸다)

  • C 와 Java 는 연산자 오버로딩을 지원하지 않는다.

  • operator(오버로딩 하고자 하는 연산자) 를 기준으로, 좌항은 연산하고자 하는 자기 자신의 개체가 되고 우항은 연산하고자 하는 대상이 된다. -> 아래 예제에서 좌항과 우항의 연산을 통해 새로운 개체를 만들어내기 때문에 매개변수에도 const, 멤버 함수에도 const 키워드를 붙여 주었다.

  • 아래의 연산자 오버로딩을 호출할 때 다음과 같이 호출도 가능하다. (WHY? 연산자도 함수이기 때문에!)

연습문제.1 Vector의 operator+() 연산자를 오버로딩해보자

  • 아래의 코드가 목표이다.

  • 결과물

연산자 오버로딩 시 문제점

  • 예를 들어 연산자 오버로딩을 사용해서 다음과 같은 코드를 만들고 싶다고 가정하자.

  • 하지만 연산자 오버로딩을 해야 하는 std::cout 은 우리가 만든 개체가 아닌, 라이브러리의 개체이다! 그것이 문제인데, 이럴 때 어떻게 문제를 해결할까?

전역 함수로써 연산자 오버로딩의 문제를 해결하자

  • 위와 같은 상황에서 다음과 같이 연산자 오버로딩의 문제를 해결할 수 있다.

  • 매개변수 두 개를 받게 되는데, 하나는 라이브러리 값을 받게 되고, 하나는 사용하고자 하는 개체의 값을 받게 된다.

  • 하지만 아래 코드에도 두 가지 문제가 존재한다.

    • 이 전역 함수를 어디에 넣는가? -> 어느 클래스에도 소속되지 않는다..

    • 전역 함수가 Vector 클래스의 private 멤버를 어떻게 읽는가?

  • 이 때 friend 함수를 쓰면 된다!

  • friend 키워드의 의미는 다음과 같다.

    • 다른 클래스나 함수가 나의 private 또는 protected 멤버에 접근할 수 있게 허용한다. -> 하지만 자바에서는 이 기능을 OOP 의 캡슐화의 기능을 헤친다고 볼 수 있기 때문에, 안티패턴으로 불리기도 한다..

  • 다음을 통해서 friend 키워드의 예를 살펴보자.

  • 다음과 같이, 클래스가 아닌 함수에 friend 키워드를 사용할 수도 있다.

  • friend 함수는 멤버 함수가 아니다!

    • 하지만 다른 클래스에서 private, protected 멤버에 접근할 수 있다.

  • 결론적으로 다음과 같이 전역 함수를 사용해 연산자 오버로딩의 문제를 해결할 수 있다.

  • 실제 사용되는 코드를 살펴보자

  • 하지만 이 코드 또한 컴파일 에러가 난다. -> 우리가 만든 void operator<<(const std::ostream& os, const Vector& rhs)`` 함수의 반환값이 void 이기 때문이다!

  • 최종적으로 정상적으로 동작하는 코드를 만들어보자. -> 반환값이 void 가 아닌 std::ostream& 이다.

연산자 오버로딩과 const

  • const 를 쓰는 이유는 무엇일까? 그 이유는 다음과 같다.

    • 멤버 변수의 값이 바뀌는 것을 방지 -> 상식적으로 생각해보자, 개체 A, B 를 연산하는데 A 의 값을 바꾸는 것이 일반적이지는 않다! -> 대부분의 경우 실수다!

    • 최대한 많은 곳에 const 를 붙이자! -> 안전한 코딩을 지향하는 방법

    • 지역(local) 변수에 까지도(모든 회사의 코딩 표준은 아니다..)

  • 매개변수에 const & 를 쓰는 이유는 무엇일까?

    • 불필요한 개체 사본이 생기는 것을 방지한다. -> 값을 바꾸지 않는 것이라면 굳이 복사를 해 메모리를 사용할 필요가 없다.

    • 멤버 변수가 바뀌는 것을 방지한다.

연산자 오버로딩을 남용하지 말아라!

  • 가독성이 심각하게 떨어질 수 있다. -> 코드의 의미를 파악하기가 어려워진다.

  • 이런식이면 차라리 함수를 만들어라! -> 실수를 줄여라!

연산자 오버로딩에서 특별한 operator=

  • operator= 에 의미는 무엇일까?

    • 생성된 개체에 다른 개체에 대입해준다는 의미 -> 복사생성자와 상당히 비슷하다. -> 차이점이라면, 대입하게 되는 개체가 생성되어 있냐(operator=), 생성하는 중이냐(복사생성자)에 차이이다.

  • 복사 생성자가 컴파일러에 의해 자동으로 만들어졌던 것처럼 operator= 또한 그렇지 않을까? -> 맞다! -> 하지만, 얕은 복사(shallow copy) 이기 때문에 직접 힙 메모리를 사용하는 경우에는, 직접 구현이 필요하다!

암시적 함수들을 제거하는 법!

  • 클래스를 생성시 딸려오는 기본 함수들은 다음과 같다.

    • 매개변수 없는 생성자

    • 복사 생성자

    • 소멸자

    • 대입(=) 연산자

  • 하나씩 지워보자

    • 기본생성자를 지우는 법

      • 생성자를 만들거나

      • 기본생성자를 private 에서 만들어주자

    • 복사생성자를 지우는 법

      • 복사생성자를 private 에서 만들어주자

    • 소멸자를 지우는 법

      • 소멸자를 private 에서 만들어주자 -> 하지만 위의 경우, 개체를 사용하고 난 후 소멸자를 호출 할 수 없기 때문에, 컴파일 에러가 발생한다.

    • 대입(=) 연산자를 지우는 법

      • 대입 연산자를 private 에서 만들어주자

Last updated