2013년 6월 18일 화요일

C++ 연산자 오버로딩


C++ 연산자 오버로딩  (출처 : winapi.co.kr)

연산자 오버로딩

연산자 오버로딩은 이미 존재하는 연산자의 기능을 조금 바꾸는 것이지 아예 새로운 연산자를 만드는 것은 아니다. 원래 C++ 언어가 제공하는 기존 연산자만 오버로딩의 대상이며 C++이 제공하지 않는 연산자를 임의로 만들 수는 없다.

class Time 
{ 
     .... 
    const Time operator +(const Time &T) const { 
          .... 
     } 
}; 
... 
C=A.operator +(B);            // C=A+B; 와 같다. 
클래스의 연산자 함수를 정의하는 방법은 다음 두가지가 있다.
① 클래스의 멤버 함수로 작성한다.
② 전역 함수로 작성한다.

리턴 타입
연산의 결과로 어떤 타입을 리턴할 것인가는 연산자별로 다르다.
(정수+실수의 리턴?, []연산자의 리턴은 멤버중 하나..등등)


const Complex operator +(const Complex &T) const { 
     Complex R(real+T.real, image+T.image); 
     return R; 
} 
const Complex operator +(const Complex &T) const { 
     return Complex(real+T.real, image+T.image); 
} 
이 코드는 앞서 만든 코드보다 훨씬 더 짧고 간략해 보일 뿐만 아니라 컴파일러의 리턴값 최적화(
Retrun Value Optimization) 기능의 도움도 받을 수 있어 훨씬 더 유리하다. 제대로 만든 컴파일러는 호출원의 대입되는 좌변에 대해 곧바로 생성자를 호출하며 불필요한 임시 객체를 만들지 않음으로써 훨씬 더 작고 빠른 코드를 생성한다.
A+B => A.operator +(B) or operator +(A,B)

    멤버 연산자 함수로 만드는 것이 더 깔끔하다. 다만 불가피하게 전역으로만 만들어야 하는 경우도 있고 =, ( ), [ ], -> 연산자들은 반드시 멤버 연산자 함수로만 만들어야 한다.
enum origin { EAST, WEST, SOUTH, NORTH }; 
origin &operator++(origin &o) 
{ 
     if (o == NORTH) { 
          o = EAST; 
     } else { 
          o=origin(o+1); 
     } 
     return o; 
} 
객체와 기본형의 연산
class Time 
{ 
     // friend const Time operator +(int s, const Time &T); // int+T friend 생략 가능하다.. 하지만 여전히 Time 클래스와 연결되어있으므로 friend로 묶여 있어도 무방. 
     const Time operator +(int s) const;                 // T+int 
     .... 
}; 
const Time Time::operator +(int s) const { // 멤버 
     .... 
} 
 
const Time operator +(int s, const Time &T) // friend전역 
{ 
     return T+s; 
} 
이미 존재하는 연산자 중에도 오버로딩의 대상이 아닌 것들이 있다. 다음 연산자들은 기능을 변경할 수 없다. 즉, 오버로딩의 대상이 아니다.

.(구조체 멤버 연산자)
::(범위 연산자)
?:(삼항 조건 연산자)
.*( 멤버 포인터 연산자)
sizeof
typeid
static_cast
dynamic_cast
const_cast
reinterpret_cast
new
delete
&&, || 논리 연산자의 경우 쇼트 서키트 기능이 동작하도록 설계되어 있지만 오버로딩되면 쇼트 서키트는 더 이상 동작하지 않는다. 문법적으로는 허용된다 하더라도 그 효과를 예측하기 어려우므로 가급적이면 이 연산자들은 오버로딩하지 말아야 한다. 사실 이 연산자들이 오버로딩되어야 하는 경우도 거의 없는 편이다.

A.operator +(B)
operator +(A,B)
둘다 있다면 어떤 함수가 실행될까?
dev-c++기준 컴파일 오류
ISO C++ says that these are ambiguous, even though the worst conversion for the first is better than the worst conversion for the second:

    const A operator+(const A&, const A A::operator+(B) const
int operator +(int a, int b)
이 함수는 정수형의 덧셈 연산을 완전히 새로 정의하는데 정수형의 덧셈은 언어의 가장 기본적인 동작이고 CPU의 원자적인 연산이기 때문에 이 동작이 바뀌게 되면 파급효과가 너무 엄청날 것이다. 그래서 컴파일러는 기본형에 대한 연산자 오버로딩은 거부하며 "최소한 하나의 피연산자는 클래스 타입이어야 한다"는 에러 메시지를 출력한다.

관계연산자

class Time 
{ 
        ... 
        bool operator ==(const Time &T) const { 
                return (hour == T.hour && min == T.min && sec == T.sec); 
        } 
        bool operator !=(const Time &T) const { 
                return !(*this == T); 
        } 
        int operator >(const Time &T) const { 
                return (hour*3600+min*60+sec > T.hour*3600+T.min*60+T.sec); 
        } 
        bool operator >=(const Time &T) const { 
                return (*this == T || *this > T); 
        } 
        bool operator <(const Time &T) const { 
                return !(*this >= T); 
        } 
        bool operator <=(const Time &T) const { 
                return !(*this > T); 
        } 
} 

증감연산자

++++A가 실행되어 A를 한 번 더 증가시키기 위해 이 연산자의 리턴 타입이 레퍼런스여야 한다. A++은 반대로 값으로 리턴.

class Time 
{ 
// time++ 
     Time &operator ++() {  
          sec++; 
          min += sec/60; 
          sec %= 60; 
          hour += min/60; 
          min %= 60; 
          return *this; 
     } 
 
// ++time 
     const Time operator ++(int dummy) { 
          Time R = *this; 
          ++*this; 
          return R; 
     } 
} 

대입 연산자

리턴형은 자신의 레퍼런스

class Person 
{ 
     .... 
    Person &operator =(const Person &Other) { 
        if (this != &Other) { 
           delete [] Name; 
           Name=new char[strlen(Other.Name)+1]; 
           strcpy(Name,Other.Name); 
           Age=Other.Age; 
        } 
        return *this; 
    } 
}; 

Person A=B; // 복사 생성자 호출
Person A,B;
A = B; // 대입 연산자 호출

Person Boy; // 여기서 디폴트 생성자.
Person Young=Boy; // 여기서 복사 생성자.. 하지만 Boy는 디폴트 생성자로 만들어져 있는데 deep 카피 시도시 문제가 생김.. 반드시 복사 생성자에서는 인자로 넘어오는 타입에 대한 NULL확인 필요.
복사 생성자
: 초기화될 때 동적할당일 경우 NULL체크를 한다.
대입 연산자
: 사용하던 메모리를 해제하고 NULL체크후 대입받는 객체에 맞게 다시 할당한다.

생성자, 소멸자, 복사생성자, 대입연산자 이중 하나라도 빠지거나 생략되면 제대로 동작하지 않는다. 추가로 상속될 경우를 대비해 소멸자는 virtual 선언한다.

복합 대입 연산자

operator + 연산자를 오버로딩 했다고 해서 operator += 까지 같이 정의되는 것이 아니다.
... 
Time &operator +=(int s) { 
... 

<< 연산자

C++ 표준이 출력 스트림으로 사용한다.
ostream& operator<<(const char *);
ostream& operator<<(char);
ostream& operator<<(short);
ostream& operator<<(int);
ostream& operator<<(long);
ostream& operator<<(float);
ostream& operator<<(double);
....

class Time 
{ 
     friend ostream &operator <<(ostream &c, const Time &T); 
     friend ostream &operator <<(ostream &c, const Time *pT); 
     ... 
}; 
ostream &operator <<(ostream &c, const Time &T) 
{ 
     c << T.hour << "시" << T.min << "분" << T.sec << "초"; 
     return c; 
} 
ostream &operator <<(ostream &c, const Time *pT) 
{ 
     c << *pT; 
     return c; 
} 

[] 연산자

기존의 배열에서 쓰이는 [] 연산자와는 구별이 된다.

DArray a[5]; 
a[4][0] = 10; // [4]는 a배열의 4번째 항목을 반환하고 클래스 내부에 정의된 [0] 항목을 반환한다. 
 
const DArray constA; 
printf("%d", constA[0]); // operator[]이 아닌 const ~ operator[] (~) const을 호출한다. 반드시 반환과 내용에 const가 있어야 컴파일 허용된다. 

class DArray 
{ 
     .... 
     ELETYPE &operator [](int idx) { // 기본 [] 연산자 
          return ar[idx]; 
     } 
     const ELETYPE &operator [](int idx) const { // const 형 인스턴스에 쓰이는  
 
[] 연산자 
     return ar[idx]; 
         } 
}; 

멤버 참조 연산자

.연산자는 오버로딩 불가하나 ->은 가능.
이 연산자의 리턴 타입은 클래스나 구조체의 포인터로 고정되어 있다. 보통 클래스에 포함된 다른 클래스 객체나 구조체의 번지를 리턴하여 포함된 객체의 멤버를 읽는 용도로 사용된다. 이 연산자를 오버로딩하면 포함 객체의 멤버를 마치 자신의 멤버처럼 액세스할 수 있다.
-> 연산자는 보통 스마트 포인터라 불리는 포인터를 흉내내는 클래스를 만들기 위해 사용되며 포인터의 유효성 점검이나 사용 카운트 유지 기능을 구현한다. 어떤 객체를 래핑하는 클래스를 만들 때 래핑한 객체가 래핑된 객체인 것처럼 동작해야 하므로 -> 연산자로 래핑된 객체의 멤버를 바로 액세스할 수 있어야 하는 것이다.


class Book 
{ 
private: 
        Author Writer; 
        ... 
public: 
        Author *operator->() {  
                return &Writer;  
        } 
} 

() 연산자

클래스는 정보와 동작을 동시에 가질 수 있으므로 세상의 모든 사물을 다 흉내낼 수 있다. 포인터를 래핑할 수도 있고 함수를 래핑할 수도 있는데 함수를 그대로 흉내내는 클래스를 정의하고 싶을 때 이 연산자를 재정의한다. () 연산자를 정의하는 클래스를 함수 객체(Functor)라고 하는데 C++ 표준 라이브러리에서 일반화된 알고리즘의 동작에 변화를 주기 위해 흔히 사용된다.

class ScoreManager 
{ 
private: 
     // 성적을 저장하는 여러 가지 멤버 변수들 
     int ar[3][5][10][4]; 
public: 
     ScoreManager() { memset(ar,0,sizeof(ar)); } 
     int &operator()(int Grade,int Class,int StNum,const char *Subj) { 
          return ar[Grade][Class][StNum][0]; 
     } 
     // [] 연산자와 같이 const형을 지원하기 위해서 
     const int &operator()(int Grade,int Class,int StNum,const char *Subj) const { 
          return ar[Grade][Class][StNum][0]; 
     } 
}; 
 
void main() 
{ 
     ScoreManager SM; 
 
     printf("1학년 2반 3번 학생의 국어 성적 = %d\n",SM(1,2,3,"국어")); 
     SM(2,3,4,"산수")=99; 
} 

new, delete 연산자

객체를 힙에 할당하는 new 연산자는 두 가지 동작을 하는데 운영체제의 힙 관리 함수를 호출하여 요청한만큼 메모리를 할당하고 이 할당된 메모리에 대해 객체의 생성자를 호출하여 초기화한다. new가 생성자를 호출하는 것은 언어의 고유한 기능이므로 사용자가 생성자 호출을 금지한다거나 할 수 없지만 객체를 위한 메모리를 할당하는 방식은 원하는대로 변경할 수 있다.

#include  
#include  
 
class AA 
{ 
public: 
        AA() 
        { 
                printf("생성자 호출\n"); 
        } 
        ~AA() 
        { 
                printf("소멸자 호출\n"); 
        } 
}; 
 
void *operator new(size_t t) 
{ 
        printf("사용자용 new\n"); 
        return malloc(t); 
} 
 
 
void operator delete(void *p) 
{ 
        printf("사용자용 delete\n"); 
        free(p); 
} 
 
 
int main() 
{ 
        AA *a = new AA(); 
        delete a; 
        system("PAUSE"); 
        return 0; 
} 
 
// 출력 결과 
/* 
사용자용 new 
생성자 호출 
소멸자 호출 
사용자용 delete 
계속하려면 아무 키나 누르십시오 . . . 
*/ 
이런 메모리 할당 기법을 정확하게 구사하기 위해서는 연산자 오버로딩 자체에 대한 이해보다는 메모리 구조나 관리 기법에 대한 이해가 더 많이 필요하다. 

댓글 없음:

댓글 쓰기

국정원의 댓글 공작을 지탄합니다.

UPBIT is a South Korean company, and people died of suicide cause of coin investment.

 UPBIT is a South Korean company, and people died of suicide cause of coin. The company helps the people who control the market price manipu...