Skip to main content

[C++ Primer Plus] Chapter 11. 클래스의 활용

C++ 기초 플러스 책을 읽고 공부한 노트입니다.




연산자 오버로딩 #

  • 시그내처(매개변수 리스트)를 다르게 제공하면 이름이 같은 여러 함수를 정의할 수 있다. 이것을 함수 오버로딩 또는 함수 다형이라고 한다.
  • 연산자 오버로딩은 그 개념을 연산자에까지 확장해서 C++ 연산자에 다중적인 의미를 부여하는 것이다.
class Time
{
private:
    int hours;
    int minutes;

public:
    Time();
    Time(int h, int m = 0);

    // 두 Time 객체의 시간을 더하는 함수 Sum
    Time Sum(const Time & other) const
    {
        Time result;

        result.minutes = minutes + other.minutes;
        result.hours = hours + other.hours + result.minutes / 60;
        result.minutes %= 60;

        return result;
    }

    // 연산자 오버로딩을 활용한 버전
    Time operator+(const Time& other) const
    {
        Time result;

        result.minutes = minutes + other.minutes;
        result.hours = hours + other.hours + result.minutes / 60;
        result.minutes %= 60;

        return result;
    }
};

int main()
{
    Time t1(3, 30);
    Time t2(2, 50);
    
    // 모두 같은 결과이다
    Time sumResult1 = t1.Sum(t2);
    Time sumResult2 = t1.operator+(t2);
    Time sumResult3 = t1 + t2;
    
    Time t3(4, 29);
    Time sumResult4 = t1 + t2 + t3; // 적법한 문법이다
    Time sumResult5 = t1.operator+(t2.operator+(t3));
}
  • 이 예제의 Sum() 함수에서 리턴값은 참조형이 될 수 없다. 리턴되는 객체는 함수 안에서 만들어지므로 함수가 종료되면 사라진다. 따라서 존재하지 않는 객체에 대한 참조가 되어버린다.

  • 오버로딩 제약
    • 적어도 하나의 피연산자가 사용자 정의 데이터형이어야 한다. 예를 들면, 표준 데이터형인 두 개의 int형의 빼기를 다른 식으로 재정의 할 수 없다.
    • 연산자 기호를 새로 만들 수 없다.
    • 본래 그 연산자에 적용되는 문법적인 규칙을 위반할 수 없다. 연산자의 우선순위도 변경할 수 없다.예를 들면, 다음과 같은 연산은 안 된다.
int x;
Time t;
% x;  // 나머지 연산자로 사용할 수 없다. 
% t;  // 따라서 오버로딩 연산자로 사용할 수도 없다.

  • 오버로딩 할 수 있는 연산자들
+ - * / % ^ & |
~ ! = < > += -= *=
/= %= ^= &= |= << >> >>=
<<= == != <= >= && || ++
-- , ->* -> () [] new delete
new [] delete []

  • 다음과 같은 연산자들은 멤버함수로만 오버로딩 할 수 있다.
연산자 이름
= 대입 연산자
() 함수 호출 연산자
[] 배열 인덱스 연산자
-> 클래스 멤버 접근 포인터 연산자



프렌드 #

  • 일반적으로 객체의 private 부분에 접근할 수 있는 유일한 통로는 public 멤버 함수들이다. 하지만 프렌드를 사용하면 객체의 private 부분에도 접근할 수 있다.

  • 만약, Time 객체와 double 형 데이터를 곱셈하는 연산자가 필요하다면 어떻게 해야할까?

class Time
{
private:
    int hours;
    int minutes;

public:
    Time operator*(double d) const
    {
        Time result;

        long totalMinutes = hours * d * 60 + minutes * d;
        result.hours = totalMinutes / 60;
        result.minutes = totalMinutes % 60;

        return result;
    }
}

int main()
{
    Time t(10, 2);
    Time mulResult1 = t * 2.75;
    Time mulResult2 = 2.75 * t; // 멤버 함수로 구현이 불가능하다.
}

  • Time형 객체 * double형 데이터operator*멤버함수로 해결 가능하다.
  • 하지만 double형 데이터 * Time형 객체의 연산은 불가능하다. 멤버 함수는 객체를 사용해서만 호출이 가능하기 때문이다.

  • 그렇다면 멤버가 아닌 함수를 만들어볼 수 있다.
  • 멤버가 아닌 오버로딩 연산자 함수는 첫번째 매개변수가 왼쪽 피연산자이며, 두번째 매개변수가 오른쪽 피연산자가 된다.
  • 하지만 멤버가 아닌 함수는 Timeprivate 데이터에 접근할 수 없는 또 다른 문제가 생긴다.
Time operator*(double d, const Time & t)
{
    // (X) 멤버가 아닌 함수는 Time의 private 데이터에 접근할 수 없다. 

    Time result;

    long totalMinutes = t.hours * d * 60 + t.minutes * d;
    result.hours = totalMinutes / 60;
    result.minutes = totalMinutes % 60;

    return result;
}

  • 이때, 멤버 함수는 아니지만 private 멤버에 접근할 수 있는 프렌드라는 특별한 함수를 사용할 수 있다.
class Time
{
private:
// …

public:
    // 앞에 friend를 붙인 함수 원형을 클래스 선언에 넣는다. 
    friend Time operator*(double d, const Time& t); 
// …
}

Time operator*(double d, const Time & t) // 정의에는 friend를 넣지 않는다.
{
    // t.operator*(d)를 사용해서 간단하게 만들 수 있다. 
    return t * d;

    //Time result;
    //
    //long totalMinutes = t.hours * d * 60 + t.minutes * d;
    //result.hours = totalMinutes / 60;
    //result.minutes = totalMinutes % 60;
    //
    //return result;
}



프렌드와 << 연산자 오버로딩 #

  • << 연산자를 오버로딩해서 cout을 사용해서 우리가 만든 Time 클래스를 출력할 수 있다면 좋겠다.
int number = 1;
cout << number; 

Time t(10, 2);
cout << t; // 이렇게 만들어 보자

  • << 연산자는 비트 조작 연산자 중에 하나이다. ostream 클래스는 이 연산자를 오버로딩해서 출력 도구로 변환시킨다. ostream 클래스 선언에는 기본 데이터형에 맞게 오버로딩된 operator<<() 정의를 가지고 있다. 그래서 cout << number; 와 같은 구문으로 기본 데이터형을 출력할 수 있다.
  • 그렇기 때문에 기본 데이터형 대신 우리가 만든 Time클래스를 넣어서 ostream에 연산자 함수의 정의를 추가하면, Time 클래스도 cout을 통해 객체의 내용을 출력할 수 있을 것이다. 하지만 직접 iostream 파일에 접근하는 것은 위험한 생각이다.
Time t(10, 2);
cout << t; 
// ostream 클래스 객체인 cout을 첫번째 피연산자로 사용하므로
// 연산자 오버로딩에 대한 정의가 ostream 클래스 내에 있어야 하겠다. 

  • 반대로 Time 클래스에게 cout을 사용하는 법을 가르칠 수 있다. 하지만 << 연산자를 다음과 같이 사용해야 하며 이것은 혼돈을 준다.
Time t(10, 2);
t << cout;   // 헷갈리는 사용법

  • 따라서 프렌드 함수를 이용해 볼 수 있겠다.
class Time
{
private:
    int hours;
    int minutes;

public:
    friend ostream& operator<<(ostream& os, const Time& t); 
	
    //...
};

ostream & operator<<(ostream & os, const Time & t)
{
    os << t.hours << " 시간, " << t.minutes << "분";

    return os;
}

int main()
{
    Time t(10, 2);
    cout << t;   // 가능
}

  • 연산자 오버로딩 함수의 리턴형이 ostream인 이유는 다음과 같은 경우 때문이다.
Time t1(10, 2);
Time t2(20, 3);
cout << t1 << t2; // (cout << t1)의 리턴형이 cout이어야 cout << t2도 가능하겠다.



클래스의 데이터형 변환 #

  • 기본 데이터형을 특정 클래스로 변환할 수 있을까? 변환 생성자를 사용하면 가능하다.
class PoundToKg
{
private:
    double pounds;
    int kg;

public:
    PoundToKg();

    explicit PoundToKg(double p) // explicit은 암시적 데이터형 변환을 못하게 한다. 
    {
        kg = p * 0.453592;
        pounds = p;
    }

    ~PoundToKg();

    void ShowKg()
    {
        cout << kg << " kg" << endl;
    }

    void ShowPounds()
    {
        cout << pounds << " lbs" << endl;
    }
};


int main()
{
    PoundToKg kg;

    // PoundToKg(double p) 생성자를 사용해서 19.6을 PoundToKg로 변환한다.

    kg = 19.6;               // 암시적 데이터형 변환. explicit로 선언되었다면 (X)이다. 
    kg = PoundToKg(19.6);    // 명시적 데이터형 변환. 
    kg = (PoundToKg) 19.6;   // 명시적 데이터형 변환의 옛날 방식. 
}

  • 암시적 데이터형 변환이 가능하면 이런 식으로도 동작 가능하다.
void Display(PoundToKg p)
{
    p.ShowKg();
    p.ShowPounds();
}

int main()
{
    Display(19.6);
}

  • 반대로 클래스를 기본 데이터형으로 변환할 수 없을까? 변환 함수를 사용하면 가능하다.
  • 변환 함수 조건
    • (1) 클래스의 멤버 함수여야 한다.
    • (2) 리턴형을 가지면 안 된다.
    • (3) 매개변수를 가지면 안 된다.
class PoundToKg
{
private:
    double pounds;
    int kg;

public:
    //...

    explicit operator double() const // explicit은 암시적 데이터형 변환을 못하게 한다. 
    {
        return pounds;
    }
}

int main()
{
    PoundToKg kg(19.6);
    double dKg;

    // PoundToKg 클래스형을 operator double() 변환함수를 사용해서 double형으로 변환한다.

    dKg = kg;         // 암시적 데이터형 변환. explicit로 선언되었다면 (X)이다.
    dKg = double(kg); // 명시적 데이터형 변환.
}