[C++ Primer Plus] Chapter 11. 클래스의 활용
Table of Contents
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형 객체
의 연산은 불가능하다. 멤버 함수는 객체를 사용해서만 호출이 가능하기 때문이다.
- 그렇다면 멤버가 아닌 함수를 만들어볼 수 있다.
- 멤버가 아닌 오버로딩 연산자 함수는 첫번째 매개변수가 왼쪽 피연산자이며, 두번째 매개변수가 오른쪽 피연산자가 된다.
- 하지만 멤버가 아닌 함수는
Time
의private
데이터에 접근할 수 없는 또 다른 문제가 생긴다.
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); // 명시적 데이터형 변환.
}