연산자 오버로딩
C++ 에서는 배열을 나타내는 클래스를 정의하고, + 연산자를 오버로딩하여 그 계산을 다음과 같이 처리할 수 있다.
evening = sam + janet; // 두 array 객체를 더한다.
이 간단한 덧셈 표기는 내부 계산은 드러내지 않고, 단지 두 객체를 더하고 있다느 사실만 강조한다. 그것이 바로 OOP가 추구하는 또 하나의 목표이다.
연산자를 오버로딩하려면, 연산자 함수(operator function)라는 특별한 함수를 사용해야 한다. 연산자 함수의 형식은 다음과 같다.
operatorop(argument-list)
여기에서 op는 오버로딩할 연산자를 나타내는 기호이다. 예를 들면, operator+ ()는 + 연산자를 오버로딩한다. operator* () 는 * 연산자를 오버로딩한다. op는 적법한 C++ 연산자여야 하낟. 새로운 기호를 사용할 수 없다. 즉, C++에는 @ 연산자가 없기 때문에 operator@ () 함수를 만들 수 없다. 그러나 operator[] () 함수는 []가 배열 인덱스 연산자이기 때문에 오버로딩할 수 있다.
뎃셈 연산자의 추가
두 개 이상의 객체들을 더할 수 있을까? 예를 들어 t1, t2, t3, t4가 모두 Time 객체일 때, 다음과 같은 뎃셈을 할 수 있을까?
t4 = t1 + t2 + t3;
이 질문에 답하려면, 구문들이 함수 호출로 어떻게 해석되는지 고려해야 한다. 덧셈 연산자는 왼쪽에서 오른쪽으로 결함하는 연산자이기 때문에, 이 구문은 먼저 다음과 같이 해석된다.
t4 = t1.operator+(t2 + t3);
그러고 나서, 함수 매개변수 자체가 함수 호출로 해석되어 다음과 같이 된다.
t4 = t1.operator+(t2.operator+(t3));
이것은 적법한가? 적법하다. 함수 호출 t2.operator+(t3)는 t2와 t3의 합을 가지고 있는 Time 객체를 리턴한다. 그리고, 이 Time 객체가 t1.operator+()함수를 호출하는 객체가 되며, 이 호출은 t1과 Time(t2와 t3의 합) 객체의 합을 리턴한다.
오버로딩 제약
오버로딩된 연산자는, 적어도 하나의 피연산자가 사용자 정의 데이터형일 것을 요구한다. 이 제약은 표준 데이터형을 위해서 사용되는 연산자들을 오버로딩하는 것을 막아 준다. 그러므로 뺄셈 연산자(-)를 두 개의 double형 값의 차(difference)가 아닌 합(sum)을 산출하도록 재정의할 수 없다.
오버로딩된 연산자를, 오리지널 연산자에 적용되는 문법 규칙을 위반하는 방식으로도 사용할 수 없다. 예를 들면, 하나의 피연산자에만 적용할 수 있도록 나머지 연산자(%)를 오버로딩 할 수없다.
int x;
Time shiva;
% x;
% shiva;
마찬가지고, 연산자 우선순위도 변경할 수 없다. 그래서 덧셈 연산자를 두 개의 객체를 더하는 연산자로 오버로딩하면, 오버로딩된 덧셈 연산자는 보통의 덧셈 연산자와 같은 우선순위를 갖는다.
연산자 기호를 새로 만들 수 없다. 예를 들면, 거듭제곱을 나타낼 목적으로 operator**() 와 같은 함수를 정의할 수 없다.
프렌드의 도입
C++는 클래스의 객체의 private 부분에 접근하는 것을 통제한다. 일반적으로 객체의 private 부분에 접근할 수 있는 유일한 통로는 public 클래스 메서드들이다. 그라나 때로는 이 제약이 너무나 엄격하여 특정 프로그래밍 문제를 해결하지 못하는 경우가 있다. 그러한 경우에 C++는 프렌드(friend)라는 또 하나의 접근 통로를 제공힌다. 프렌드는 다음과 같은 세가지 형태로 사용된다.
- 프렌드 함수
- 프렌드 클래스
- 프렌드 멤버 함수
함수를 어떤 클래스에 대해 프렌드로 만들면, 그 프렌드 함수는 클래스의 멤버 함수들이 가지는 것과 동등한 접근 관한을 갖는다.
프렌드를 만드는 방법을 살펴보기 전에, 그들이 왜 필요한지부터 알아보자. 흔히, 어떤 클래스에 대해 이항 연산자를 오버로딩하면, 프렌드를 만들 필요성이 생긴다. Time 객체에 하나의 실수로 곱할 때가 바로 그런 상황이다.
A = B * 2.75;
다음과 같은 멤버 함수 호출로 번역된다.
A = B.operator*(2.75);
그러나 다음과 같은 표현은 어떻게 될까?
A = 2.75 * B;
개념적으로 생각하면, 2.75 * B는 B * 2.75와 같아야 한다. 그러나 2.75 * B는 2.75가 Time 객체형이 아니기 때문에 멤버 함수에 대응시킬 수 없다. 왼쪽 피연산자가 호출하는 객체라는 사실을 다시 기억하라. 2.75는 객체가 아니다. 그렇기 때문에 컴파일러는 그 표현을 멤버 함수 호출로 번역하지 못한다.
이 작은 문제를 해결하는 한 가지 방법은, 자신을 포함하여 모든 사람들에게 프로그램을 작성할 때 2.75 * B라고 쓰지 말고, B * 2.75로 쓰라고 일러두는 것이다. 이것은 프로그래머 편을 들면서 사용자에게 조심하라고 말하는 것이므로 결코 OOP적인 해결책이 이니다.
이것을 해결할 수 있는 또 다른 가능성은, 멤버가 아닌 함수를 사용하는 것이다. 멤버가 아닌 함수는 객체를 사용하여 호출하지 않는다. 그 대신 멤버가 아닌 함수는, 객체를 포함하여 어떤 값을 명시적 매개변수로 사용하여 호출한다. 그래서, 컴파일러는 위와 같은 표현을 그 함수의 원형은 다음과 같을 것이다.
Time operator* (2.75, B);
그 함수의 원형은 다음과 같을 것이다.
Time operator*(double m, const Time & t);
멤버가 아닌 연산자 오버로딩 함수를 사용하면, 2.75 * B와 같이, 우리가 처음에 원했던 순서로 피연산자들을 사용할 수 있다. 그러나 그것은 새로운 문제를 일으킨다. 멤버가 아닌 함수들은 클래스에 들어 있는 private 데이터에 직접 접근할 수 없다.
프렌드 생성하기
프렌드 함수를 만드는 첫 번째 단계는, 클래스 선언에 원형을 넣는 것이다. 이때 friend라는 키워드를 앞에 붙여야 한다.
friend Time operator*(double m, const Time & t);
이 원형은 두 가지 함축적인 의미를 가지고 있다.
- operator*() 함수는, 클래스 선언 안에 선언되었지만 멤버 함수가 아니다. 그러므로 멤버 연산자를 사용하여 호출되지 않는다.
- operator*() 함수는, 그것이 비록 멤버 함수는 아니지만 멤버 함수와 동등한 접근 권한을 가진다.
두 번째 단계는 함수 정의를 작성하는 것이다. 그것은 멤버 함수가 아니기 때문에 Time:: 제한자를 사용하지 않는다. 또한, 그 정의에 friend라는 키워드도 사용하지 않는다. 그 정의는 다음과 같아야 한다.
Time operator*(double m, const Time & t)
{
Time result;
long totalminutes = t.hours * mult * 60 + t.minutes * mult;
result.hours = totalminutes / 60;
result.minutes = totalminutes % 60;
return result;
}
이와 같이 선언하고 정의하면, 다음과 같은 구문은
A = 2.75 * B;
다음과 같이 번역되어, 금방 정의한 멤버가 아닌 프렌드 함수를 호출한다.
A = operator*(2.75, B);
간단히 말해서, 어떤 클래스에 대한 프렌드 함수는 멤버 함수와 동등한 접근 권한을 가지는, 멤버가 아닌 함수이다.
프렌드는 OOP에 어울리지 않는다?
언뜻 보면 멤버가 아닌 함수들이 private 데이터에 접근하는 것을 허용하기 때문에, 프렌드가 데이터 은닉이라는 OOP의 철학을 위반하고 있는 것처럼 보인다. 그러나 그것은 너무 좁은 견해이다. 그렇게 생각하지 말고, 프렌드 함수들은 클래스를 위한 확장 인터페이스의 일부라고 생각해야 한다. 개졈상으로 보았을 때 double형 값에 Time값을 곱하는 것은, Time 값에 doble형 값을 곱하는 것과 같다. 전자가 프렌드 함수를 요구하고 후자가 멤버 함수를 요구하는 것은, 개념상에 차이가 있기 때문이 아니라 C++의 문법 때문이다.
요악하면, 클래스 메서드와 프렌드는 단순히 클래스 인터페이스를 나타내는 두 개의 서로 다른 메커니즘이다.
프렌드: << 연산자의 오버로딩 첫 번째 버전
Time 클래스에게 cout을 사용한는 법을 가르치려면, 프렌드 함수를 사용해야 한다. 그 이유는 다음과 같은 구문이
cout << trip;
두 개의 객체를 사용하고 있지만, ostream 클래스 객체(cout)을 첫 번째 피연산자로 사용하기 때문이다. 만약 <<를 오버로딩하기 위해 Time 클래스의 멤버 함수를 사용한다면, 멤버 함수를 사용하여 * 연산자를 오버로딩했을 때 그랫던 것처럼, Time 객체가 첫 번째 피연산자가 되어야 한다. 그것은 우리가 << 연산자를 다음과 같은 방식으로 사용해야 한다는 것을 의미한다.
trip << cout // operator<<()가 Time 멤버 함수일 경우에
이것은 당연히 혼동을 줄 것이다. 그러나 프렌드 함수를 사용한다면, 우리는 그 연산자를 다음과 같이 오버로딩할 수 있다.
void operator<<(ostream & os, const Time & t)
{
os << t.hours >> "시간, " << t.minutes << "분";
}
이제 우리는 다음과 같은 구문을 사용할 수 있다.
cout << tipe;
오버로딩 <<의 두 번째 버전
앞에서 방금 설명한 구현은 하나의 문제점을 가지고 있다. 위와 같은 구문은 잘 동작할 것이다. 그러나 그 오버로딩 << 연산자는 우리가 평상시에 cout을 사용하는 방식인, 다음과 같은 표현에서는 동작하지 않는다.
cout << "여행 일시: " << trip << "(화요일)\n"; // 동작하지 않는다.
이것이 동작하지 않는 이유와, 동작하게 하려면 무여슬 해야 하는지 알기 위해서는, cout의 동작이 어떻게 이루어지는지 먼저 알 필요가 있다. 다음과 같은 구문들이 있을 때
int x = 5;
int y = 8;
cout << x << y;
C++는 마지막의 출력 구문을 왼쪽에서부터 오른쪽으로 읽는다. 이것은 그 출력 구문이 다음과 같은 구문과 동등하다는 것을 의미한다.
(cout << x) << y;
iostream 파일에 정의된 대로, << 연산자는 ostream 객체를 왼쪽 피연산자로 요구한다. cout << x라는 표현은, cout이 ostream 객체이기 때문에 그 요구를 명백하게 만족시킨다. 그러나 그 출력 구문은 또한, (cout << x)라는 표현 자체가 ostream형의 객체일 것을 요구한다. 그래서 ostream 클래스는 ostream 객체를 리턴하도록 operator<<() 함수를 구현하고 있다. 특별히 이 경우에는 호출한 객체 cout을 리턴한다. 따라서 (cout << x)라는 표현 자체가 ostream 객체가 되므로, << 연산자의 왼쪽에 사용될 수 이다.
프렌즈 함수에 대해서도 같은 접근 방식을 취할 수 있다. 즉, operator<<() 함수가 ostream 객체에 대한 참조를 리턴하도록 재정하면 된다.
ostream & operator<<(ostream & os, const Time & t)
{
os << t.hours << "시간, " << t.minutes << "분";
return os;
}
리턴형이 ostream &이라는 것에 주목하라. 이것은 그 함수가 ostream 객체에 대한 참조를 리턴하는 것을 의미한다. 프로그램이 처음 시직할 때 그 함수에 객체에 대한 참도를 전달하기 때문에, 결과적으로 그 함수의 리턴값은 그 함수에 전달된 객체 자신이다. 즉, 다음과 같은 구문은
cout << trip;
다음과 같은 함수 호출이 된다.
operator<<(cout, trip);
그리고 이 함수 호출은 cout 객체를 리턴한다. 그래서 이제 다음과 같은 구문도 바르게 동작한다.
cout << "여행 일시: " << trip << " (화요일)\n";
오버로딩 연산자: 멤버 함수와 멤버가 아닌 함수
많은 연산자들에 대해서 연산자 오버로딩을 구현할 때, 멤버 함수로 구현할 것인지 멤버가 아닌 함수로 구현할 것인지를 선택해야 한다. 일반적으로는 멤버가 아닌 함수가, 클래스의 private 데이터에 직접 접근할 수 있는 프렌드 함수이다. 예를 들면, Time 클래스를 위한 뎃셈 연산자를 생각해 보자. 그것은 Time 클래스 선언에 다음과 같은 원형을 가지고 있다.
Time operator+(const Time & t) const;
이것 대신에, Time 클래스는 다음과 같은 원형을 가질 수도 있다.
friend Time operator+(const Time & t1, const Time & t2); // 멤버 아닌 함수
이 덧셈 연산자는 두 개의 피연산자를 요구한다. 멤버 함수 버전은 this 포인터를 통하여 암시적으로 하나가 전달되고, 다른 하나는 함수 매개변수로 명시적으로 전달된다. 프렌드 함수 버전은 둘 다 매개변수로 전달된다.
두 원형 모두 T2 + T3 표현에 대해 바르게 동작한다. 여기서 T2와 T3는 Time형의 객체이다. 즉, 컴파일러는 다음과 같은 구문을
T1 = T2 + T3;
다음 둘 중의 어느 하나로 변환할 수 있다.
T1 = T2.operator+(T3);
T1 = operator+(T2, T3);
주어진 하나의 연산자를 정의할 때, 사용자는 두 형식 중 반드시 어느 한 형식을 선택해야 한다. 두 형식이 동일한 표현식에 해당하므로, 두 형식을 모두 정의하는 것은, 컴파일 에러를 일으키는 모호성 에러로 간주된다. 그렇다면 어느 형식을 사용하는 것이 좋을까? 앞에서 설명한 바와 같이, 일부 연산자들에 대해서는 멤버 함수가 유일하고도 적절한 선택이다. 그 밖의 경우에는 별다른 차이가 없다. 클래스 설계에 따라서는 간혹 특별히 그 클래스에 대한 데이터형 변환을 정의했을 경우에, 멤버가 아닌 버전이 유리할 수도 있다.
한걸음 더: 오버로딩 된 연산자 또 오버로딩하기
뺄셈 연산자는 정확히 두 개의 피연산자를 요구하므로 이항 연산자(binary operator)라고 부른다. 둘째, -x에서와 같이 하나의 피연산자에 사용되었을 때 그것은 마이너스 부호 연산자이다. 이 경우에는 피연산자에 사용되었을 때 그것은 마이너스 부호 연산자이다. 이경우에는 피연산자를 한개만 요구하므로 단항 연산자(unary operator)라고 부른다.
자동 변환과 클래스의 데이터형 변환
C++는 그 값의 데이터형을 대입밭는 입장에 있는 변수와 동일한 데이터형으로 자동으로 변환한다. 예를 들어, 다음과 같은 구문들은 수치 데이터형의 변화를 일으킨다.
long count = 8; // int형 값 8을 long형으로 변환한다.
double time = 11; // int형 값을 11을 double형으로 변환한다.
int side = 3.33; // double형 값 3.33을 int형 값 3으로 변환한다.
다양한 수치 데이터형들이 모두 기본적으로 동일한 수(number)를 나타내고, C++가 데이터형 변환을 위한 내부 규칙을 적용하기 때문에, 이러한 대입 구분들은 바르게 작동한다. 그러나 이러한 변화 과정에서 정확성에 손실을 입을 수 있다. 예를 들어, 3.33을 int형 변수 side에 대입하면, side에는 값 3만 저장되고 소수부 0.33은 날아 간다.
C++는 호횐되지 않는 데이터형은 자동으로 변환하지 않는다. 예를 들어, 다음과 같은 구문은
int * p = 10; //데이터형 불일치
왼쪽은 포인터형인 반면에 오른쪽은 수이기 때문에 동작하지 않는다. 컴퓨터가 주소를 내부적으로 하나의 정수로 표시한다고 하더라도, 정수와 포인터는 개념적으로 아주 다른 것이다.
기본 데이터형이나 다른 어떤 클래스와 충분히 관련되어 있어, 서로 간에 변환할 수 있는 클래스를 정의할 수 있다. 그러한 경우에, 자동 변환을 할 것인지 강제 변환을 할 것인지 사용자가 C++에게 지시할 수 있다.
class Stonewt
{
private:
enum {Lbs_per_stn = 14}; // 스톤당 파운드 수
int stone; // 정수부(스톤)
double pds_left; // 소수부(파운드)
double pounds; // 파운드로 나타낸 전체 무게
public:
Stonewt(double lbs); // double형 파운드를 위한 생성자
Stonewt(int stn, double lbs); // stone, lbs를 위한 생성자
Stonewt(); // 디폴트 생성자
~Stonewt();
void show_lbs() const; //파운드 형식으로 출력
void show_stn() const; // 스톤과 파운드 조합 형식으로 출력
}
enum은 클래스용 상수를 정의하는 편리한 방법을 제공한다. 그것은 클래스에서 사용할 정수들을 제공하낟. 최신 컴파일러들은 다음과 같은 대체 문구도 허용한다.
static const int Lbs_per_stn = 14;
Stonewt 클래스가 3개의 생성자를 가지고 있는 것에 주목한다. 이들은 Stonewt 객체를 파운드로 나타낸 하나의 부동소수점 수로 초기화하거나, 또는 스톤과 파운드 조합으로 초기화할 수 있도록 허용한다. 또는 초기화하지 않고 Stonewt 객체를 생성할 수 있다.
이 경우에는 자동 디폴트 생성자가 괜찮기 때문에 클래스가 파괴자를 선언할 필요는 없다. 반면에, 명시적 선언은 사용자로 하여금 생성자를 선언해야만 할 미래의 어느 시험에 대비할 수 있게 해 준다.
또한, Stonewt 클래스는 2개의 출력 함수를 제공한다. 하나는 파운드 형식으로 출력하고, 다른 하나는 파운드와 스톤의 조합 형식으로 출력한다.
C++에서, 하나의 매개변수를 취하는 생성자는 그 매개변수 데이터형의 값을 클래스형으로 변환하는 설계도처럼 동작한다. 그러므로 다음 생성자는 double형 값을 Stonewt형 값으로 변환하는 명령으로 사용한다.
Stonewt(double lbs); // double을 Stonewt 객체로 변환하는 템플릿
double형 값을 Stonewt형 값으로 변환하는 구문 역할을 한다. 즉, 다음과 같은 코딩을 사용할 수 있다.
Stonewt myCat; // Stonewt 객체를 생성
myCat = 19.6; // Stonewt(double)을 사용하여 19.6을 Stonewt으로 변환한다.
프로그램은 Stonewt(double) 생성자를 사용하여 Stonewt 임시 객체를 생성한다. 이때 19.6을 초기화값으로 사용한다. 그러고 나서 멤버별 대입으로 임시 객체의 내용을 myCat에 복사한다. 이 과정은 명시적 데이터형 변환이 필요 없이 자동으로 이루어지므로 암시적 데이터형 변환이라고 한다.
하나의 매개변수만으로 사용할 수 있는 생성자만이 이와 같은 변환 함수로 동작한다. 예를 들면 다음과 같은 생성자는 2개의 매개변수를 사용하므로 데이터형 변환에 사용할 수 없다.
Stonewt(int stn, double lbs); // 변환 함수가 아님
그러나 두 번째 변수에 디폴트 값을 제공할 경우, int형 변환 함수의 역할을 한다.
Stonewt(int stn, double lbs = 0); // int를 Stonewt로 변환
생성자를 자동 데이터형 변환 함수로 동작하게 하는 것은 멋진 기능처럼 보인다. 그러나 프로그래머들이 C++를 많이 경험하게 되면, 자동 데이터형 변환이 항상 바람직한 것은 아니고, 오히려 예상하지 못한 부작용을 일으킬 수도 있다는 사실을 알게 된다. 그래서 최신 C++ 시스템에는 자동 데이터형 변환을 못하게 하는 explicit라는 새로운 키워드가 추가되었다. 즉, 그 생성자를 다음과 같이 선언할 수 있다.
explicit Stonewt(double lbs); // 암시적 데이터형 변환을 허용하지 않는다.
이것은 앞의 예와 같은 암시적 데이터형 변환은 못하게 막는다. 그러나 다음과 같은 명시적 데이터형 강제 변환은 여전히 허용한다.
Stonewt myCat = 19.6;
myCat = 19.6; // Stonewt 객체를 생성한다.
myCat = Stonewt(19.6); // 맞다, 명시적 데이터형 변환
myCat = (Stonewt) 19.6; // 맞다, 명시적 데이터형 변환의 옛날 형식
컴파일러는 Stonewt (double) 함수를 언제 사용할까? 키워드 explicit가 선언에 사용된 경우에, Stonewt(double) 은 명시적 데이터형 변환에만 사용된다. 그렇지 않은 경우에는 다음과 같은 암시적 데이터형 변환에도 사용된다.
- Stonewt 객체를 double형 값으로 초기화할 때
- Stonewt 객체에 double형 값을 대입할 때
- Stonewt형 매개변수를 기대하는 함수 double형 값을 전달할 때
- Stonewt형 값을 리턴하도록 선언된 함수가 duble형 값을 리턴하려고 시도할 때
- 앞의 네 상황에서 double형 대신 모호하지 않게 double형으로 변환할 수 있는 내정 데이터형을 사용할 때
마지막 다섯 번쨰 상황을 좀 더 자세히 살펴보자.
Stonewt Jumbo(7000); // int를 double로 변환, Stonewt(double) 사용
Jumbo = 7300; // int를 double로 변환, Stonewt(double) 사용
이 2단계 변화 과정은 double형으로의 변환이 모호하지 않은 경우에만 동작한다. 즉, 그 클래스가 Stonewt(long) 생성자도 따로 정의해 두었다면, 컴파일러는 이들 구문의 수행을 거부할 것이다. 왜냐하면 int형은 long형으로 또는 double형으로 어느 쪽으로도 변환할 수 있으므로 호출이 모호해지기 때문이다.
변환 함수
하나의 수(number)를 Stonewt 객체로 변환한다. 이 과정을 반대로 할 수는 없을까? 즉, 다음과 같이 Stonewt 객체를 double형 값으로 변환할 수 있을까?
Stonewt wolfe(285.7);
double host = wolfe;
이것은 가능하다. 그러나 생성자를 사용하지 않는다. 생성자는 어떤 데이터형을 클래스형으로 변환하는 것만 허용한다. 이 과정을 반대로 하기 위해서는, 변환 함수(conversion function)라고 부르는 특별한 형태의 C++ 연산자 함수를 사용해야 한다.
변환 함수는 사용자 정의 강제 데이터형 변환이다. 따라서 일반적인 강제 데이터형 변환(type cast)처럼 사용하면 된다. 예를 들어, Stonewt를 double로 변환하는 함수를 정의했다면, 다음과 같은 변환을 사용할 수 있다.
Stonewt wolfe(285.7);
double host = double(wolfe);
double thinker = (double)wolfe;
또는 이 문제를 컴파일러가 스스로 처리하게 할 수 있다.
Stonewt wells(20, 3);
double star = wells;
오른쪽이 Stonewt형이고 왼쪽이 double형이라는 것을 인식한 컴파일러는, 사용자가 이 문제를 해결할 수 있는 변환 함수를 정의했는지 찾아본다.
그렇다면 변환 함수는 어떻게 작성해야 할까? typeName형으로 변환하려면, 변환 함수를 다음과 같은형식으로 사용한다.
operator typeName();
다음과 같은 사항을 기억해 두자.
- 변환 함수는 클래스의 메서드여야 한다.
- 변환 함수는 리턴형을 가지면 안 된다.
- 변환 함수는 매개변수를 가지면 안 된다.
예를 들어, double형으로 변환하는 함수는 원형이 다음과 같을 것이다.
operator double();
typeName부분이 어떤 데이터형으로 변환할 것인지 알려 준다. 그래서 리턴형이 필요 없다. 그 함수가 클래스 메서드라는 사실은, 그것이 특정 클래스 객체에 의해 호출되어야 한다는 것을 의미하낟. 객체가 그 함수에게 변환할 값을 알려 준다. 그러므로 변환 함수는 매개변수를 필요로 하지 않는다.
Stonewt 객체를 int형과 double형으로 각각 변환하는 함수를 추가하려면, 클래스 선언에 다음과 같은 원형을 추가해야 한다.
operator int();
operator double();
데이터형 변환의 자동 적용
cout과 함깨 int (poppins)를 사용한다. 그렇게 하지 않고 다음과 같이 명시적인 강제 데이터형 변환을 생략했다고 가정해 보자
cout << "Poppins: " << poppins << "파운드\n";
이러한 경우에 다음 구문과 같이 컴파일러가 암시적 변환을 사용할까?
double p_wt = poppins;
대답은 '아니오'이다. p_wt 예에서는 poppins가 double형으로 변환되어야 한다고 문맥이 알려 준다. 그러나 cout 예에서는 int형으로 변환해야 하는지 double형으로 변환해야 하는지 문맥이 알려 주는 것이 없다. 이렇게 정보가 충분하지 않을 경웨, 컴파일러는 모호한 변환이 사용되고 있다고 불평할 것이다. 어떤형을 사용할 것인지 알려 주는 것이 구문 안에 전혀 없다.
흥미롭게도, 클래스에 double 변환 함수 하나만을 정의했다면, 컴파일러가 이 문제로 불평을 하지 않는다. 이 경우에는 단지 하나의 변환만이 가능하므로 상황이 모호하지 않기 때문이다.
대입 구분에서도 이와 비슷한 상황을 만날 수 있다. 현재 클래스 선언을 그대로 사용한다면, 모호가기 때문에 컴파일러는 다음과 같은 대입 구문을 거부할 것이다.
암시적 변환을 자동으로 수행하는 함수의 문제점은, 원하지 않을 때에도 그러한 변환을 수행 할 수 있다는 것이다. 예를 들어, 잠이 부족하여 깜박 졸다가 그만 다음과 같은 코드를 작성했다고 가정하자.
int ar[20];
Stonewt temp(14, 4);
int Temp = 1;
cout << ar[temp] << "!\n"; // Temp 대신 temp를 사용했다.
배열 인텍스로 정수 대신 객체를 사용한 이와 같은 큰 실수를 컴파일러가 잡아내리라고 기대할 것이다. 그러나 Stonewt 클래스에 operator int() 가 정의되어 있으므로, Stonewt 객체 temp는 int형 값 200으로 변환되어 배열 인덱스로 사용된다. 이것을 통해 우리가 알 수 있는 것은 명시적 변환을 사용하여 암시적 변환의 가능성을 없애는 것이 가장 좋다는 것이다. C++98버전에서는 키워드 explicit는 변환 함수와 어울려 사용되지 않는다. 그러나 C++11에서 그러한 한계를 제거하였다. 그러므로, 사용자는 C++11을 통해서 변환 연산자가 명시적임을 선언할 수 있다.
Class Stonewt
{
// 변환 함수들
explicit operator int() const;
explicit operator double() const;
}
이러한 선언이 있다면, 사용자는 연산자들을 통해서 형 변환을 사용할 수 있다. 명시적으로 호출할 경우에는 동일한 작업을 수행하는 비변환 함수로 변환 함수를 대체하면 된다. 즉, 다음과 같은 것을
Stonewt::operator int() {return int (pounds + 0.5);}
다음과 같이 대체할 수 있다.
int Stonewt::Stone_to_Int() {return int (pounds + 0.5);}
이것은 다음과 같은 구문을 허용하지 않는다.
int plb = poppins;
그러나 변환이 꼭 필요하다면 다음과 같은 구문을 허용한다.
int plb = poppins.Stone_to_Int();
'C++ > C++ 기초 플러스 6판(Book)' 카테고리의 다른 글
[C++] 클래스의 상속 (0) | 2025.03.10 |
---|---|
[C++] 클래스와 동적 메모리 대입 (0) | 2025.03.09 |
[C++] 객체와 클래스 (0) | 2025.03.07 |
[C++] 메모리 모델과 이름 공간 (0) | 2025.03.06 |
[C++] 함수의 활용 (0) | 2025.03.05 |