책정리/혼자 연구하는 C,C++ 2

28장 연산자 오버로딩

GONII 2015. 2. 27. 13:03

28.1 연산자 함수

28.1.1 기본형의 연산자

연산자를 오버로딩 할 수 있다는 것은 C++ 언어의 큰 특징이며 클래스가 타입임을 보여주는 단적인 예라고 할 수 있다. 조금 어렵기는 하지만 문법이 체계적이어서 이해하고 나면 언어의 질서를 느낄 수 있으며 오히려 재미있기도 하다.

덧셈 연산문의 예

int i1=1, i2=2 ;

double d1=3.3, d2=4.4 ;

int i = i1 + i2 ; // 정수 덧셈

double d = d1 + d2 ; // 실수 덧셈

   

정수형과 실수형은 길이도 다르고 비트 구조도 상이해서 각 타입을 더하는 알고리즘이 분명히 다르겠지만 똑같은 연산자로 두 타입의 덧셈이 가능한 것이다.

이렇게 되는 이유는 덧셈 연산자가 피연산자의 타입에 따라 오버로딩 되어 있기 때문이다.

+ 기호를 덧셈을 하는 함수의 이름이라고 했을 때 이 함수의 원형은 다음과 같이 오버로딩 되어 있을 것이다.

int +(int, int) ;

double +(double, double) ;

   

+라는 똑같은 모양의 연산자로 일관되게 덧셈 연산을 할 수 있는 것은 다형성의 예이다.

  • 예제 ComplexAdd

#include <iostream>

using namespace std ;

   

class complex

{

private :

double real ;

double image ;

   

public :

complex() { }

complex(double r, double i) : real( r ), image( i ) { }

void outComplex() const

{

cout << real+image << endl ;

}

} ;

   

void main ( void )

{

complex c1(1.1, 2.2) ;

complex c2(3.3, 4.4) ;

c1.outComplex() ;

c2.outComplex() ;

   

complex c3 ;

// c3 = c1 + c2 ;        // 에러 발생

c3.outComplex() ;

}

28.1.2 연산자 함수

  • 예제 TimeAdd

#include <iostream>

using namespace std ;

   

class time

{

private :

int hour, min, sec ;

   

public :

time() { }

time(int h, int m, int s) { hour = h ; min = m ; sec = s ; }

void outTime()

{

cout << hour << ":" << min << ":" << sec << endl ;

}

const time addTime(const time &t) const

{

time r ;

r.sec = sec + t.sec ;

r.min = min + t.min ;

r.hour = hour + t.hour ;

   

r.min += r.sec/60 ;

r.sec %= 60 ;

r.hour += r.min/60 ;

r.min %= 60 ;

return r ;

}

} ;

   

void main ( void )

{

time a(1,50,1) ;

time b(2,50,2) ;

time c;

   

a.outTime() ;

b.outTime() ;

c = a.addTime(b) ;

c.outTime() ;

}

사실 연산자는 모양이 좀 특이한 함수라고 볼 수 있는데 인수를 취한다는 것과 연산 결과를 리턴한다는 점에서 함수와 공통적이다.

연산자 함수의 이름은 키워드 operator 다음에 연산자 기호를 써서 작성하는데 연산자 기호를 명칭으로 쓸 수 없으므로 operator라는 키워드를 앞에 두는 것이다.

덧셈 연산자 함수의 이름은 operator+ 가 된다.

위 예제에서 addTime이라는 함수의 이름을 operator+로 바꾸고 c = a + b ; 로 바꾼다.

c = a.operator + (b) ; // c = a + b ; 와 동일

c=a+b는 연산문이고 c=a.operator+(b)는 함수 호출문의 형태를 띠고 있을 뿐 실행되는 코드는 둘다 동일하다.

   

addtime 함수와 operator+연산자 함수의 차이점

  1. 연산자 형태의 호출 방식이 길이가 짧아 파이팅하기 편리하며 오타가 발생할 가능성도 극히 낮다.
  2. 연산자 함수는 호출 형식이 연산문 형태로 작성되기 때문에 훨씬 더 직관적이고 기본형의 연산 방법과 일치하므로 사용하기 쉽다.
  3. 연산자는 함수와는 달리 우선 순위와 결합 방향의 개념이 있어 괄호를 쓰지 않아도 연산 순서가 자동으로 적용되어 편리하다.

       

28.1.3 연산자 함수의 형식

  • 클래스의 연산자 함수를 정의 하는 방법
    • 클래스의 멤버 함수로 작성한다.
    • 전역 함수로 작성한다.

       

  • 멤버 연산자 함수의 기본 형식

    리턴 타입 class::operator 연산자 ( 인수 목록 )

    {

    함수 본체 ;

    }

       

    :: 인수의 타입

    연산자 함수의 인수란 피연산자를 의미하는데 함수를 호출하는 자기 자신(this)과 함수로 전달되는 인수가 연산 대상이다. 이항 연산자의 경우 멤버 연산자 함수를 호출하는 객체가 좌변이 되고 인수로 전달되는 대상이 우변이 된다.

    객체는 값을 넘길 수 있지만 아무래도 기본형보다는 덩치가 크기 때문에 값으로 넘기면 비효율적이므로 레퍼런스로 넘기는 것이 유리하다. & 기호를 빼고 값으로 넘겨도 동작에는 별 이상은 없지만 객체가 커지면 다소 느릴 것이다.

       

  • 연산자 함수로 피연산자를 넘기는 방법
    • 값으로 넘기는 방법 ( 객체가 커지만 효율이 나쁘다 )
    • 포인터로 넘기는 방법 ( 효율은 좋지만 호출 구문이 요상해진다.)
    • 레퍼런스로 넘기는 방법 ( 효율과 직관적인 표기 모두 가능하다 )

       

    C++이 레퍼런스 타입을 지원하는 주된 이유 중의 하나가 바로 객체 연산식의 직관적인 표현을 위해서이다.

       

    :: 인수의 상수성

    피연산자로 전달된 인수는 보통 읽기만 한다. 그래서 연산자 함수로 전달되는 인수는 읽기 전용의 const로 받는 것이 좋다.

       

    :: 함수의 상수성

    멤버 연산자 함수가 호출 객체의 상태를 바꾸지 않을 경우는 원칙에 따라 const 함수로 지정하는 것이 좋다.

       

    :: 임시 객체의 사용

    임시 객체는 호출 객체와 피연산자의 값을 변경하지 않고 연산 결과를 잠시 저장하기 위한 용도로 사용하는 것이다.

    임시객체를 사용하지 않을 경우 +연산자의 좌변 객체가 변경되어 버리므로 (+=) 연산이 되어 버린다.

       

    :: 리턴 타입

    연산의 결과로 어떤 타입을 리턴할 것인가는 연산자별로 다르다. 정수끼리 더하면 정수가 되고 실수끼리 곱하면 실수가 되는 것처럼 객체 대한 연산 결과는 보통 객체와 같은 타입이 되지만 반드시 그런 것은 아니다. 논리 연산자의 경우는 bool형이나 int형이 리턴 될 수도 있고 첨자 연산자[ ]의 경우처럼 특수한 연산자는 멤버 중의 하나를 리턴하는 경우도 있다.

    연산자 함수가 객체를 리턴 할 때 레퍼런스를 리턴 할 것인가, 값을 리턴 할 것인가는 연산자에 따라 다르다.

       

    :: 리턴 타입의 상수성

    리턴 타입의 상수성도 경우에 따라 다른데 객체 타입을 리턴하는 함수는 보통 상수 객체를 리턴해야한다.

       

    :: 생성자의 활용

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) ;

}

이 코드는 오른쪽이 훨씬 더 짧고 간략해 보일뿐만 아니라 컴파일러의 리턴값 최적화(ROV : REturn Value Optimization) 기능의 도움도 받을 수 있어 훨씬 더 유리하다. 제대로 만든 컴파일러는 호출원의 대입되는 좌변에 대해 곧바로 생성자를 호출하며 불필요한 임시 객체를 만들지 않음으로써 훨씬 더 작고 빠른 코두를 생성한다.

   

:: 본체

연산자 함수의 본체에는 연산자에 요구되는 논리적인 연산 코드를 작성한다.

클래스가 표현하는 대상에 따라 연산하는 방법이 고유하고 특수하기 때문에 클래스를 만든 사람이 연산 방법 자체를 정의할 수 있어야 하며 이런 정의를 가능하도록 하는 C++의 문법적인 장치가 바로 연산자 오버로딩이다. 모든 클래스에 대해, 모든 연산자에 대해 절대적으로 적용되는 법칙 같은건 없으며 클래스별로 연산자별로 규칙이 달라진다.

28.2 전역 연산자 함수

28.2.1 전역 연산자 함수

전역 연산자 함수는 클래스 외부에 존재하되 인수로 클래스의 객체를 받아들인다.

  • 예제 TimeOpPlus

#include <iostream>

using namespace std ;

   

class time

{

friend const time operator+(const time &t1, const time &t2) ;

private:

int hour, min, sec ;

public :

time() { }

time(int h, int m, int s) { hour=h; min=m; sec=s; }

void outTime()

{

cout << hour << ":" << min << ":" << sec << endl ;

}

} ;

   

const time operator+(const time &t1, const time &t2)

{

time r ;

   

r.sec = t1.sec + t2.sec ;

r.min = t1.min + t2.min ;

r.hour = t1.hour + t2.hour ;

   

r.min += r.sec / 60 ;

r.sec %= 60 ;

r.hour += r.min / 60 ;

r.min %= 60 ;

   

return r ;

}

   

void main ( void )

{

time a(1,1,1) ;

time b(2,2,2) ;

time c ;

   

a.outTime() ;

b.outTime() ;

c = a + b ;

c.outTime() ;

}

연산자 함수를 멤버로 정의하지 않는 대신 operator+ 전역 함수를 friend로 지정하여 자신의 모든 멤버를 자유롭게 액세스 할 수 있도록 허락한다.

   

friend선언을 생략해 버리면 수많은 에러 메세지가 출력될 것이다.

   

객체를 위한 연산자를 오버로딩하는 두 가지 방법, 즉 멤버로 만드는 방법과 전역으로 만드는 방법이 있다.

   

멤버 연산자 함수

a.operator+(b)

   

전역 연산자 함수

operator+(a,b)

   

두 형식의 연산자 함수는 정의하는 위치만 다를뿐 큰 차이점은 없다.

클래스의 객체를 다루는 연산이라면 가급적이면 클래스에 소속되는 것이 캡슐화의 원칙에 부합되므로 멤버 연산자 함수로 만드는 것이 더 깔끔하다. 다만 불가피하게 전역으로만 만들어야 하는 경우도 있고 =,(),[],-> 연산자들은 반드시 멤버 연산자 함수로만 만들어야 한다.

   

  • 예제 EnumOperator

#include <iostream>

using namespace std ;

   

enum origin { EAST, WEST, SOUTH, NORTH } ;

origin &operator++(origin &o)

{

if ( o == NORTH )

{

o = EAST ;

}

else

{

o = origin(o+1) ;

}

return o ;

}

   

void main ( void )

{

origin mark = WEST ;

int i ;

   

for ( i = 0 ; i < 7 ; i ++ )

{

cout << ++mark << endl ;

}

}

28.2.2 객체와 기본형의 연산

연산자를 오버로딩하면 연산문으로 객체끼리 연산할 수 있는 것과 마찬가지로 객체를 정수나 실수형 같은 기본형이나 다른 객체와도 연산할 수 있다. 사실 클래스가 타입이므로 굳이 객체와 기본형을 구분할 필요가 없으며 논리적으로 의미만 있다면 오버로딩하기에 따라서 임의 타입의 객체끼리 연산 가능하다.

  • 예제 timePlusInt

#include <iostream>

using namespace std ;

   

class time

{

private :

int hour, min, sec ;

   

public :

time() {}

time(int h, int m, int s){ hour = h ; min = m ; sec = s ; }

void outTime()

{

cout << hour << min << sec << endl ;

}

const time operator+(int s) const

{

time r = *this ;

   

r.sec += s ;

r.min += r.sec/60 ;

r.sec %= 60 ;

r.hour += r.min/60 ;

r.min %= 60 ;

return r ;

}

} ;

   

void main ( void )

{

time a( 1, 2, 3 ) ;

   

a.outTime() ;

a = a + 5 ;

a.outTime() ;

}

operator+ 멤버 연산자 함수가 int형의 s를 인수로 받아들여 이 값을 임시 객체 r의 sec에 더한 후 자리 올림 처리하고 r을 리턴했다. 객치기리 더할 때는 시분초를 모두 더하지만 정수형의 초와 더할 때는 sec만 더하는 정도의 차이밖에는 없다. 단 1초라도 더하면 분, 시도 영향을 받을 수 있으므로 자리 올림 처리는 생략할 수 없다.

28.2.3 오버로딩 규칙

  1. 연산자 오버로딩은 이미 존재하는 연산자의 기능을 조금 바꾸는 것이지 아예 새로운 연산자를 만드는 것은 아니다.

    원래 C++ 언어가 제공하는 기존 연산자만 오버로딩의 대상이며 C++이 제공하지 않는 연산자를 임의로 만들 수는 없다. 예를 들어 C++은 누승 연산자를 제공하지 않는데(대신 pow라는 표준 함수를 제공한다.) 새로운 연산자를 헝요하면 구문 분석 단계에서 모호함이 발생한다. 그리고 새로 만들어진 연산자의 우선 순위와 결합 순서를 어떻게 정할 것인지도 문제가 된다. 오버로딩이란 이미 존재하는 것을 중복 정의하는 것이지 없는 걸 아예 새로 만드는 것이 아니다.

  • 이미 존재하는 연산자 중에서도 오버로딩의 대상이 아닌 것들이 있다.
    • 오버로딩이 안되는 연산자

. (구조체 멤버 연산자)

:: (범위 연산자)

?: ( 삼항 조건 연산자)

.* ( 멤버 포인터 연산자)

sizeof

typeid

static_cast

dynamic_cast

const_cast

reinterpret_cast

new

delete

이 연산자들은 C++의 클래스와 관련된 중요한 동작을 하기 때문에 클래스를 기반으로 하는 연산자 오버로딩의 재료로 스기에는 무리가 있다. 이런 특수한 연산자만 빼고 나머지 42개나 되는 연산자들은 모두 오버로딩할 수 있다.

콤마 연산자, &&, || 논리 연산자는 문법적으로 허용된다 하더라도 그 효과를 예측하기 어려우므로 가급적이면 이 연산자들은 오버로딩하지 말아야 한다.

  1. 기존 연산자의 기능을 바꾸더라도 연산자의 본래 형태는 변경할 수 없다.

    여기서 본래 형태라고 하는 것은 피연산자의 개수와 우선 순위를 말한다. + 연산자는 원래 피연산자를 두 개 취하는 이항 연산자 이므로 오버로딩된 후에도 이항 연산자이여야 하며 반드시 피 연산자 두개를 가져야 한

    다.

const time time::operator+ (time &t) // 가능

const time time::operator+ (int i) // 가능

const time operator+ (time &t, int i) // 가능

const time time::operator+ (time &t1, time &t2) // 불가능

const time operator+ (time &t) // 불가능

const time operator+ ( void ) // 불가능

  1. 한 클래스가 하나의 연산자를 여러 가지 피연산자 타입에 대해 오버로딩 할 수 있다.

    오버로딩이란 인수의 개수나 타입이 다르면 항상 성립하므로 여러 개의 피연산자에 대한 연산자를 제공할 수 있다

  2. 오버로딩된 연산자의 피연산자 중 적어도 하나는 사용자 정의형이어야 한다.

    연산자의 기능을 바꾸는 목적은 객체에 대한 고유한 연산 방법을 정의하기 위한 것이므로 반드시 객체와 관련있는 연산자만 중복할 수 있다. C++이 기본적으로 제공하는 타입에 대해서는 연산자를 오버로딩할 수 없다. 기본형에 대한 연산자 오버로딩을 하게 되면 컴파일러는 기본형에 대한 연산자 오버로딩은 거부하며 "최소한 하나의 피연산자는 클래스 타입이어야 한다"는 에러 메시지를 출력한다.

  3. 강제적인 규칙은 아니지만 연산자의 논리적 의미는 가급적 유지하는 것이 바람직 하다.

    +연산자를 오버로딩한다면 어떤 클래스에 대해서라도 덧셈의 의미를 가지는 연산을 하는 것이 좋다. 그래야 연산자에 대한 사용자들의 기존 상식을 보호할 수 잇다.

28.3 오버로딩의 예

28.3.1 관계 연산자

동일한 타입의 두 객체에 대해 상등 및 대소를 비교

클래스별로 비교 방법이 틀리기 때문에 비교를 위해 관계 연산자를 오버로딩

  • 예제 TimeRelation

#include <iostream>

using namespace std ;

   

class Time

{

private :

int hour, min, sec;

public:

Time() {}

Time(int h, int m, int s) : hour(s), min(m), sec(s) {}

void outTime()

{

cout << hour << " : " << min << " : " << sec << endl ;

}

bool operator==( const Time &t) const

{

return ( hour == t.hour && min == t.min && sec == t.min ) ;

}

bool operator!= ( const Time &t ) const

{

return !( *this == t ) ;

}

bool operator >( const Time &t ) const

{

if ( hour > t.hour ) return 1 ;

if ( hour < t.hour ) return 0 ;

if ( min > t.min ) return 1 ;

if ( min < t.min ) return 0 ;

if ( sec > t.sec ) return 1 ;

if ( sec < t.sec ) return 0 ;

return 0 ;

}

   

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 ) ;

}

};

   

void main ( void )

{

Time a(1,1,1);

Time b(1,1,1);

   

if ( a == b )

{

cout << "같다" << endl ;

}

else

{

cout << "다르다" << endl ;

}

}

28.3.2 증감 연산자

++연산자는 피연산자를 1증가시키는 단항 연산자

  • 예제 TimePlusPlust

#include <iostream>

using namespace std ;

   

class Time

{

private:

int hour, min, sec;

public:

Time() {}

Time( int h, int m, int s ) : hour(h), min(m), sec(s) {}

void outTime()

{

cout << hour << " : " << min << " : " << sec << endl ;

}

// 전위++a

Time& operator++()

{

sec++;

min += sec / 60;

sec %= 60;

hour += min / 60 ;

min %= 60;

return *this ;

}

// 후위 a++

const Time operator++ ( int dummy )

{

Time r = *this ;

++*this;

return r ;

}

};

   

void main()

{

Time a( 1, 1, 1 ) ;

Time b;

   

b = ++a;

a.outTime();

b.outTime();

b = a++;

a.outTime();

b.outTime();

}

28.3.3 대입 연산자

자신과 같은 타입의 다른 객체를 대입받을 때 사용하는 연산자

  • 예제 person3

#include <iostream>

using namespace std ;

class person

{

private :

char *name;

int age;

public:

person() : age(0)

{

name = new char[1];

name[0] = NULL;

}

person( const char* aName, int aAge ) : age(aAge)

{

name = new char[strlen(aName)+1] ;

strcpy( name, aName ) ;

}

// 복사생성자

person( const person &other ) : age(other.age)

{

name = new char[strlen(other.name)+1];

strcpy( name, other.name ) ;

}

~person()

{

delete [] name ;

}

void outPerson()

{

cout << "name : " << name << ", age : " << age << endl ;

}

   

};

   

void main()

{

person a("aaa", 3 ) ;

a.outPerson();

   

person b;

// 대입연산 실행됨, 복사생성자와는 다르다

// person b(a); <-와 같이 해야 복사생성자 실행

// 얕은 복사만 이루어지므로 name이 같은 곳을 바라봄, delete[] name을 두번 실행하다 오류발생

b = a ;

b.outPerson();

}

// 대입연산자 오버로딩

person& operator= ( const person &other )

{

// 자기 대입 방지

if ( this != &other )

{

// 원래 name은 지워주고 다시 new

delete[] name;

name = new char[strlen(other.name)+1];

strcpy( name, other.name ) ;

age = other.age;

}

return *this;        

}

:: 대입 후 리턴되는 값

대입 연산자의 리턴타입이 person&인 이유는 a=b=c 식의 연쇄적 대입이 가능해야 하기 때문임

   

:: 올바른 디폴트 생성자

동적 할당을 하는 클래스의 경우 포인터를 NULL로 초기화해서는 안됨

person3의 디폴트 생성자가 할당하는 1아트는 자리만 지키는 플레이스 홀더(PlaceHolder) 역할을 한다. 아무 짝에도 쓸모 없는 것 같지만 name이 반드시 동적 할당된 메모리임을 보장하여 이 버퍼를 참조하는 모든 코드를 정규화시키는 효과가 있음

모든 멤버 함수는 name의 길이가 얼마이든지 무조건 할당되어 있다는 가정 하에 name을 안심하고 액세스 할 수 있음

   

:: 동적 할당 클래스의 조건

   

:: 복사 대입 연산자

operator+연산자를 오버로딩했다고 해서 operator+=까지 같이 정의되는 것은 아니다

   

:: 복사 생성 및 대입 금지

class person

{

private:

char* name;

person( const person &other );

person &operator= (const person &other ) ;

...

복사 생서자와 대입 연산자를 선언하되 둘다 private 영역에 두면 호출할 수 없다

본체 내용은 작성하지 않는다

28.3.4 << 연산자

C++의 표준 스트림 출력 객체인 cout은 << 연산자를 오버로딩하여 이 연산자의 우변을 표준 출력(모니터)으로 내보내는 기능을 제공한다. <<연산자 다음의 피연산자가 정수든 실수든 포인터든 거의 가리지 않고 출력되는데 이렇게 되는 이유는 cout 객체의 소속 클래스인 ostream에 다음과 같은 여러 원형의 << 멤버 연산자 함수가 오버로딩되어 있기 때문이다.

ostream& operator<<(const char*);

ostream& operator<<(char);

ostream& operator<<(short);

ostream& operator<<(int);

ostream& operator<<(long);

ostream& operator<<(float);

ostream& operator<<(double);

   

cout으로 클래스 객체를 출력하려면 <<연산자를 cout이 클래스 객체를 인식하도록 추가로 오버로딩해야 한다.

  • 예제 coutTime

#include <iostream>

using namespace std;

   

class Time

{

friend ostream &operator<<(ostream &c, const Time &t);

friend ostream &operator<<(ostream &c, const Time *pt);

private:

int hour, min, sec;

public :

Time(){}

Time( int h, int m, int s ) : hour(h), min(m), sec(s) {}

void outTime()

{

cout << hour << " : " << min << " : " << sec << endl;

}

};

   

ostream &operator<<(ostream &c, const Time &t)

{

c << t.hour << "시" << t.min << "분" << t.sec << "초" << endl ;

return c;

}

ostream &operator<<(ostream &c, const Time *pt)

{

c << *pt;

return c;

}

   

void main()

{

Time A(1,1,1);

Time *p;

   

p = new Time(2,2,2);

cout << "현재 시간은" << A ;

cout << "현재 시간은" << p ;

   

delete p;

}

ostream 객체와 Time형 객체 또는 포인터를 인수로 취하는 ostream << 전역 연산자를 두 벌 정의하고 이 연산자 함수를 Time의 프렌드로 지정했다. 출력을 위해 자신의 모든 멤버를 읽을 수 있도록 권한을 주어야 한다. 컴파일러는 cout << A 연산문을 만났을 때 ostream 클래스의 멤버 연산자 함수 <<를 검색해 보고 Time을 인수로 취하는 함수가 있는지 조사한 후 멤버 중에 그런 함수가 없으면 전역 함수를 찾는다. 결국 cout << A 연산문은 operator <<(cout, A) 전역 연산자 함수 호출문으로 해석되어 cout으로 Time 객체의 시분초 멤버를 순서대로 출력한다.

28.3.5 [ ] 연산자

[ ] 연산자는 배열에서 첨자 번호로부터 요소를 찾는다. 반드시 멤버 함수로만 정의할 수 있으며 전역 함수로는 정의할 수 없다. 여러 가지 자료의 집합을 다루는 클래스에서 이 연산자를 오버로딩하여 원하는 대로 기능을 부여할 수 있다.

class DArray

{

...

ELETYPE &operator[](int idx)

{

return ar[idx];

}

};

[ ]연산자가 다른 연산자들과 다른 특이한 점이라면 대입식의 좌변과 우변에 모두 쓸 수 있다는 점이다. 그래서 상수 객체에 대해서도 쓸 수 있는데 const 버전의 [ ] 연산자도 중복 정의해야 한다.

const ELETYPE &operator[](int idx) const

{

return ar[idx];

}

  • 예제 TimeIndex

#include <iostream>

using namespace std;

   

class Time

{

private:

int hour, min, sec;

   

public :

Time(){}

Time( int h, int m, int s ) : hour(h), min(m), sec(s) {}

void outTime()

{

cout << hour << " : " << min << " : " << sec << endl;

}

   

int &operator [](int what)

{

switch(what)

{

case 0:

return hour;

case 1:

return min;

default:

case 2:

return sec;

}

}

const int &operator [](int what) const

{

switch(what)

{

case 0:

return hour;

case 1:

return min;

default:

case 2:

return sec;

}

   

}

};

   

void main()

{

Time A(1,1,1);

const Time B(7,7,7);

   

A[0] = 12;

cout << "현재 시간은 " << A[0] << endl;

//B[0] = 8;

cout << "현재 시간은 " << B[0] << endl;

}

Time 객체를 구성하는 hour, min, sec 멤버는 배열이 아니지만 [ ]연산자를 오버로딩하여 [0]이면 시, [1]이면 분, [2]면 초를 리턴하도록 했다. 외부에서는 마치 이 객체를 구성하는 시분초 멤버가 배열에 속한 요소인 것처럼 사용하는 것이 가능하다.

C/C++ 언어의 [ ] 연산자는 포인터 연산을 하도록 정의되어 있으므로 피연산자 중 하나는 반드시 포인터이고 나머지 하나는 반드시 정수여야 한다. 그러나 오버로딩되면 어디까지나 함수에 불과하므로 임의의 타입을 인수로 전달받을 수 있다.

  • 예제 StuList

#include <iostream>

//#include <string.h>

using namespace std;

   

class StuList

{

private :

struct Student

{

char name[10];

int StNum;

} s[30];

   

public :

StuList()

{

strcpy(s[0].name, "abc"); s[0].StNum = 1;

strcpy(s[1].name, "job"); s[1].StNum = 3;

strcpy(s[2].name, "cabin"); s[2].StNum = 6;

strcpy(s[3].name, "yoo"); s[3].StNum = 8;

strcpy(s[4].name, "lee"); s[4].StNum = 15;

strcpy(s[5].name, "??????"); s[5].StNum = 100;

}

int operator[](const char *name)

{

for ( int i = 0 ; ; i++ )

{

if( strcmp(s[i].name, name) == 0 ) return s[i].StNum;

if( s[i].name[0] == '?' ) return -1;

}

}

};

   

void main()

{

StuList sl;

   

cout << "abc의 학번은" << sl["abc"] << endl ;

}

[ ] 연산자를 일종의 검색 연산자로 의미를 변경하여 활용하는 것이다.

28.3.6 멤버 참조 연산자

클래스나 구조체의 멤버를 참조하는 연산자에는 .과 -> 두 가지가 있다.

.연산자는 클래스를 프로그래밍하는 너무 기본적인 연산자이므로 오버로딩할 수 없으며 객체의 포인터로부터 멤버를 읽는 ->연산자는 오버로딩 대상이다. 이 연산자는 다른 연산자와는 다른 독특한 오버로딩 규칙이 적용되는데 원래 이항 연산자이지만 오버로딩하면 단항 연산자가 되며 전역 함수로는 정의할 수 없고 클래스의 멤버 함수로만 정의할 수 있다. 멤버 함수이면서 단항이므로 인수를 취하지 않는다. 이 연산자의 리턴 타입은 클래스나 구조체의 포인터로 고정되어 있다. 보통 클래스에 포함된 다른 클래스 객체나 구조체의 번지를 리턴하여 포함된 객체의 멤버를 읽는 용도로 사용된다.

  • 예제 MemberAccessOp

#include <iostream>

using namespace std;

   

struct author

{

char name[32];

char tel[24];

int age;

};

   

class book

{

private:

char title[32];

author writer;

public :

book(const char *aTitle, const char *aName, int aAge)

{

strcpy(title, aTitle);

strcpy(writer.name, aName);

writer.age = aAge;

}

author *operator->() { return &writer; }

const char *getTitle(){ return title; }

};

   

void main()

{

book hyc("혼자 연구하는 c/c++", "김상형", 25);

cout << "제목 : " << hyc.getTitle() << " 저자 : " << hyc->name << " 나이 : " << hyc->age << endl;

}

hyc->name으로 writer 포함 객체의 멤버를 읽었는데 보다시피 name이 hyc객체의 멤버인 것처럼 사용되고 있다. 원래 ->연산자의 좌변에는 포인터만 올 수 있지만 오버로딩되면 ->연산자가 포인터를 리턴하기 때문에 객체가 와도 상관없다. 이 표현식은 컴파일러에 의해 다음과 같이 해석된다.

hyc.operator->()->name

->연산자가 &writer를 리턴하므로 이 포인터로부터 writer의 멤버를 바로 액세스 할 수 있다. 포함 객체의 멤버를 읽기 위해 hyc.writer.name 이런식으로 .연산자를 두 번 사용하는 것은 허가되지 않는데 writer가 private 액세스 속성을 가지고 있기 때문이다. -> 연산자는 숨겨진 멤버의 포인터를 읽어 줌과 동시에 이 멤버에 속한 멤버를 바로 액세스 할 수 있도록 중계하는 역할을 한다.

->연산자는 보통 스마트 포인터라 불리는 포인터를 흉내내는 클래스를 만들기 위해 사용되며 포인터의 유효성 점검이나 사용 카운트 유지 기능을 구현한다. 어떤 객체를 래핑하는 클래스를 만들 때 래핑한 객체가 래핑된 객체인 것처럼 동작해야 하므로 -> 연산자로 래핑된 객체의 멤버를 바로 액세스 할 수 있어야 하는 것이다.

28.3.7 () 연산자

( )도 함수를 호출하는 일종의 연산자이다. 다른 연산자와는 달리 항의 개수가 정해져 있지 않다는 것이 특징인데 호출하는 함수에 따라 이항일 수도 있고 단항일 수도 있고 세 개 이상의 인수를 취할 수도 있다. 그래서 오버로딩할 때도 인수의 개수를 원하는 대로 취할 수 있고 인수의 개수나 타입이 다르면 얼마든지 오버로딩 가능하며 인수에 디폴트값을 저장할 수도 있다.

  • 예제 FunCallOp

#include <iostream>

using namespace std;

   

class sum

{

public:

int operator()(int a, int b, int c, int d)

{

return a+b+c+d;

}

double operator()(double a, double b)

{

return a+b;

}

};

   

void main()

{

sum s;

cout << s(1, 2, 3, 4) << endl;

cout << s(1.2, 3.4) << endl;

}

이 연산자의 좌변은 항상 호출 객체이므로 전역 함수로는 정의할 수 없으며 반드시 멤버 함수로만 정의해야 한다.

  • 예제 ScoreManager

#include <iostream>

using namespace std;

   

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 int &operator()(int grade, int Class, int StNum, const char *subj) const

{

return ar[grade][Class][StNum][0];

}

};

   

void main()

{

ScoreManager sm;

   

cout << "1학년 2반 3번 학생의 국어 성적 = " << sm(1,2,3,"국어") << endl;

sm(2,3,4,"수학") = 99;

cout << "2학년 3반 4번 학생의 수학 성적 = " << sm(2,3,4,"수학") << endl;

}

28.3.8 new, delete

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

delete함수도 두 가지 동작을 하는데 소멸자를 먼저 호출하여 객체를 정리하고 다음으로 operator delete를 호출하여 객체가 사용하던 메모리를 해제한다.

대량의 메모리를 효율적으로 관리하기 위해 가상 메모리를 직접 다루고 싶다거나 미리 할당해 놓은 메모리 풀을 조금씩 돌려가며 사용하고 싶을 때 new, delete 연산자를 오버로딩 한다.

Win32의 가상 메모리는 예약과 확정이라는 두 단계의 메모리 할당 방식이 있고 각각의 메모리 페이지에 대해 읽기, 쓰기 권한을 지정하 ㄹ수 있어 할당 속도가 빠르고 안전성이 높아 직접 관리할 경우 힘을 쓰는 것보다 더 효율적이다. 특히 객체의 크기가 클 때 효과적이다. 또한 할당, 해제가 아주 빈번하다면 충분한 크기의 메모리 큐를 만들고 으용 프로그램이 메모리를 회전시키는 방법도 쓸 수 있다.

  • 예제 newOverload

#include <iostream>

using namespace std;

   

void *operator new(size_t t)

{

return malloc(t);

}

   

void operator delete(void *p)

{

free(p);

}

   

void main()

{

int *pi = new int;

*pi = 1234;

cout<< *pi << endl;

delete pi;

}

객체의 배열을 할당 및 해제하는 new[], delete[]도 물론 오버로딩할 수 있다. 이런 메모리 할당 기법을 정확하게 구사하기 위해서는 연산자 오버로딩 자체에 대한 이해도 중요하지만 메모리 구조나 관리 기법에 대한 이해가 더 많이 필요하다.

28.4 문자열 클래스

28.4.1 Str 클래스

C는 문자열을 기본 타입으로 제공하지 않고 문자형 배열로 표현하기 때문에 대입, 연결, 비교, 추가 등의 모든 연산을 함수로만 해야 한다.

그래서 C++에서는 보통 문자열을 클래스로 작성하는데 이 클래스는 문자열 표현에 필요한 모든 멤버를 포함하고 있으며 문자열의 길이에 따라 배열을 자동으로 늘리는 편리한 기능까지 가지고 있다. 또한 다양한 연산자를 문자열에 대해 직접 사용할 수 있도록 하여 기본 타입과 똑같은 방법으로 문자열을 다룰 수 있다.

  • 예제 Str

#include <iostream>

#include <stdarg.h>        // 가변인자 헤더

using namespace std;

   

class Str

{

private :

friend ostream &operator<<(ostream &c, const Str &s);

friend const Str operator +(const char *ptr, Str &s);

friend bool operator ==(const char *ptr, Str &s);

friend bool operator !=(const char *ptr, Str &s);

friend bool operator >(const char *ptr, Str &s);

friend bool operator <(const char *ptr, Str &s);

friend bool operator >=(const char *ptr, Str &s);

friend bool operator <=(const char *ptr, Str &s);

protected:

char *buf;

int size;

public :

Str();

Str(const char *ptr);

Str(const Str &other);

explicit Str(int num);

virtual ~Str();

   

int length() const{ return strlen(buf); }

Str& operator =(const Str &other);

Str& operator +=(Str &other);

Str& operator +=(const char *ptr);

char& operator [](int idx) const { return buf[idx]; }

operator const char*() { return (const char *)buf; }

operator int() { return atoi(buf); }

const Str operator +(Str &other) const;

const Str operator +(const char *ptr) const ;

bool operator ==(Str &other) { return strcmp(buf, other.buf)==0; }

bool operator ==(const char *ptr) { return strcmp(buf, ptr)==0; }

bool operator !=(Str &other) { return strcmp(buf, other.buf)!=0; }

bool operator !=(const char *ptr) { return strcmp(buf, ptr)!=0; }

bool operator >(Str &other) { return strcmp(buf, other.buf)>0; }

bool operator >(const char *ptr) { return strcmp(buf, ptr)>0; }

bool operator <(Str &other) { return strcmp(buf, other.buf)<0; }

bool operator <(const char *ptr) { return strcmp(buf, ptr)<0; }

bool operator >=(Str &other) { return strcmp(buf, other.buf)>=0; }

bool operator >=(const char *ptr) { return strcmp(buf, ptr)>=0; }

bool operator <=(Str &other) { return strcmp(buf, other.buf)<=0; }

bool operator <=(const char *ptr) { return strcmp(buf, ptr)<=0; }

void Format(const char *fmt,...);

};

   

// 디폴트 생성자

Str::Str()

{

size = 1;

buf = new char[size];

buf[0] = 0;

}

// 문자열로부터 생성

Str::Str(const char *ptr)

{

size = strlen(ptr)+1;

buf = new char[size];

strcpy(buf, ptr);

}

// 복사 생성자

Str::Str(const Str &other)

{

size = other.length()+1;

buf = new char[size];

strcpy(buf, other.buf);

}

// 정수형 변환 생성자

Str::Str(int num)

{

char temp[128];

   

itoa(num, temp, 10);

size = strlen(temp)+1;

buf = new char[size];

strcpy(buf,temp);

}

   

// 소멸자

Str::~Str()

{

delete[] buf;

}

   

// 대입 연산자

Str &Str::operator =(const Str &other)

{

if( this != &other )

{

size = other.length()+1;

delete[] buf;

buf = new char[size];

strcpy(buf, other.buf);

}

return *this;

}

   

// 복합 연결 연산자

Str &Str::operator +=(Str &other)

{

char *old;

old = buf;

size += other.length();

buf = new char[size];

strcpy(buf, old);

strcat(buf, other.buf);

delete []old;

return *this;

}

Str& Str::operator +=(const char *ptr)

{

Str t(ptr);

return *this+=t;

//return *this+=Str(ptr);

}

   

// 연결 연산자

const Str Str::operator +(Str &other) const

{

Str t;

   

delete [] t.buf;

t.size = length()+other.length()+1;

t.buf = new char[t.size];

strcpy(t.buf, buf);

strcat(t.buf, (const char*)other);

return t;

}

const Str Str::operator +(const char *ptr) const

{

Str t(ptr);

return *this+t;

}

   

// 출력 연산자

ostream &operator <<(ostream &c, const Str &s)

{

c << s.buf;

return c;

}

   

// 더하기 및 관계 연산자

const Str operator +(const char *ptr, Str &s) { return Str(ptr)+s; }

bool operator ==(const char *ptr, Str &s) { return strcmp(ptr, s.buf)==0; }

bool operator !=(const char *ptr, Str &s) { return strcmp(ptr, s.buf)!=0; }

bool operator >(const char *ptr, Str &s) { return strcmp(ptr, s.buf)>0; }

bool operator <(const char *ptr, Str &s) { return strcmp(ptr, s.buf)<0; }

bool operator >=(const char *ptr, Str &s) { return strcmp(ptr, s.buf)>=0; }

bool operator <=(const char *ptr, Str &s) { return strcmp(ptr, s.buf)<=0; }

   

// 서식 조립 함수

void Str::Format(const char *fmt,...)

{

char temp[1024];

va_list marker;

   

va_start(marker, fmt);

vsprintf(temp, fmt, marker);

*this = Str(temp);

}

void main()

{

Str s="125";

int k;

k = (int)s+123;

cout << k << endl;

   

Str s1("문자열");        // 문자열로 생성자

Str s2(s1);                        // 복사 생성자

Str s3;                                // 디폴트 생성자

s3 = s1;                        // 대입 연산자

   

// 출력 연산자

cout << "s1 = " << s1 << ", s2 = " << s2 << ", s3 = " << s3 << endl;

cout << "길이 = " << s1.length() << endl;

   

// 정수형 변환 생성자와 변환 연산자

Str s4(1234);

cout << "s4 = " << s4 << endl;

int num = int(s4)+1;

cout << "num = " << num << endl;

   

// 문자열 연결 테스트

Str s5 = "First";

Str s6 = "Second";

cout << s5 + s6 << endl;

cout << s5+"Third" << endl;

cout << "Zero"+s5 << endl;

cout << "s1은 " +s1+"이고 s5는 "+s5+"이다"<< endl;

s5+=s6;

cout << "s5+=s6 = " << s5 << endl;

s5 += "concatination";

cout << "s5 += concatination = " << s5 << endl;

   

if( s1 == s2 )

{

cout << "두문자열은 같다" << endl;

}

else

{

cout << "두 문자열은 다르다." << endl;

}

   

// char* 형과의 연산 테스트

Str s7;

s7 = "상수 문자열";

cout << s7 << endl;

char str[128];

strcpy(str, s7);

cout << str << endl;

   

// 첨자 연산자 테스트

Str s8("Index");

cout << "s8[2] = " << s8[2] << endl;

s8[2]='k';

cout << "s8[2] = " << s8[2] << endl;

   

// 서식 조립 테스트

Str sf;

int i = 9876;

double d = 1.234567;

sf.Format("서식 조립 가능. 정수=%d, 실수=%2f",i,d);

cout << sf << endl;

}

// 예제 따라치면 *this+Str(ptr);과 *this+=Str(ptr)에서 재귀호출됨.

// *this를 하면 (const char*)buf를 리턴하는데

// return buf+Str(ptr)이 다시 operator+(const char*)를 재귀호출 하는 듯

28.4.2 메모리 관리

Str클래스의 멤버 변수는 단 두 개밖에 없다. 동적인 길이를 가지는 문자열을 표현해야 하므로 문자형 포인터buf가 이런 역할을 한다. size는 할당된 메모리양을 기억하되 사실 꼭 필요하지는 않다. 문자열의 길이에 널 종료문자분을 더하면 버퍼 길이는 언제나 구할 수 있되 여유분을 더 할당한거나 할 때는 길이에 대한 정보가 필요해지므로 미리 포함시켜 둔 것이다.

인수를 취하는 않는 디폴트 생성자는 1바이트만 할당하고 빈 문자열로 초기화한다. 객체를 만든 후 곧바로 출력할 수도 있기 때문에 buf를 NULL로 초기화해서는 안되며 빈 문자열이라도 가지고 있어야 한다. 가장 자주 사용하는 생성자는 const char*형 인수를 받아들여 문자열 상수로부터 객체를 초기화하는 생성자이다. 이 생성자는 문자열의 길이에 널 종료 문자만큼 더해 buf를 할당하고 ptr의 문자열을 buf에 복사한다.

동적으로 할당하는 버퍼를 사용하므로 복사 생성자를 반드시 정의해야 한다. 그렇지 않으면 객체끼리 대입할 때 디폴트 복사 생성자가 얕은 복사를 하므로 같은 버펄르 두 객체가 가리키게 될 것이다.

모든 생성자가 초기 문자열의 길이만큼 동적 할당을 하므로 파괴자는 반드시 이 메모리를 해제해야 한다. 또한 대입 연산자도 정의해야 하는데 대입은 실행 중에 언제든지 일어날 수 있으므로 메모리를 무조건 할당하는 것이 아니라 메모리를 먼저 반납하고 새로 할당해야 한다. 생성자 외에 문자열의 길이를 변경하는 =, +=, Format 함수에서도 버퍼의 길이를 동적으로 관리한다.

28.4.3 타입 변환

cout으로 문자열을 출력하기 위해 << 연산자를 정의하여 프렌드로 등록했다. buf 문자열을 cout으로 보내고 연쇄적으로 출력할 수 있도록 지침대로 스트림 객체의 레퍼런스를 리턴한다.

Str 클래스는 문자열이나 정수로부터 객체를 생성하는 생성자를 제공하므로 char*나 int 타입으로부터 초기화할 수 있다.

Str객체는 const char*와 int 두 개의 변환 함수를 제공하는데 이 두 연산자에 의해 Str이 잠시 정수형이 되거나 문자형 포인터로 될 수 있다. 그래서 다음 코드는 모두 정상적으로 컴파일되고 잘 실행된다.

Str s = "125";

int k;

k = (int)s+123;

Str t = "String";

char *p = strchr((cnost char*)t, 'r');

Str 객체에 정수 형태의 문자열이 들어있다면 (int)캐스트 연산자로 정수를 추출할 수 있다. 이 변환 연산자는 atoi함수로 문자열을 정수로 바꿔 리턴한다.

const char*연산자는 Str의 객체의 buf멤버 주소를 리턴하는데 여기서 객체가 표현하는 문자열이 들어 있으므로 이 포인터만 알면 문자열을 바로 사용할 수 있다. 단 객체 외부에서 이 버퍼를 함부로 조작해서는 안 되므로 반드시 상수 지시 포인터를 리턴해야 하며 타입이 맞는 위치에만 쓸 수 있다.

[ ]연산자는 배열상의 한 요소에 대한 레퍼런스를 리턴하는데 Str객체를 마치 문자형 배열과 같은 방법으로 사용할 수 있다. 레퍼런스를 리턴하므로 이 연산자로 요소를 읽는 것은 물론이고 직접 변경하는 것도 가능하다. 단, 상수 객체일 경우는 상수 버전의 [ ]연산자 함수로 읽는 것만 가능하며 요소값을 변경하진느 못한다. [ ]연산자의 본체는 buf에 첨자 연산을 한 결과를 바로 리턴하도록 되어 있다. 그래서 문자형 배열에서와 마찬가지로 첨자 범위를 점검하지 못하는 한계를 가지고 있는데 이 문제를 해결하려면 if문으로 배열의 범위를 점검하기만 하면 된다.

28.4.4 연결 및 비교 연산자

문자열을 연결할 때는 +=과 +연산자를 사용한다. +=은 호출하는 객체 자신에게 문자열을 연결하고 +는 피연산자를 변경하지 않고 임시 객체에 두 문자열을 연결하여 리턴한다는 점이 다르다. +=연산자는 Str객체를 인수로 받는 함수와 const char*타입을 인수로 받는 함수로 오버로딩 되어 있다.

문장려을 연결하려면 객체 자신의 문자열과 인수로 전달된 객체의 문자열이 모두 필요하고 두 문자열을 합쳤을 때 버퍼 길이가 늘어나므로 재할당해야 한다. 그래서 원래 버퍼의 포인터를 old에 잠시 대피해 놓고 합친 길이만큼 buf를 재할당한 후 원본 문자열 복사, 인수 문자 문자열 연결을 한다. 작업을 마친 후 원본 문자열은 더 이상 필요가 없으므로 삭제한다. const char*를 피연산자로 취하는 +=연산자는 ptr로부터 임시 객체를 만든 후 Str을 인수로 받는 함수를 호출한다.

   

+연산자는 문자열끼리 연결하여 임시 객체를 만드는데 피연산자를 변경시키지는 않는다. 이 함수는 피연산자의 타입과 순서에 따라 다음 세가지 버전이 모두 필요하다.

  • Str + Str : 문자열 객체끼리 더하는 멤버 함수
  • Str + ptr : 문자열 객체에 const char*형의 문자열을 더하는 멤버 함수
  • ptr + Str : const char*형의 문자열에 문자열 객체를 더하는 프렌드 함수

비교 연산자도 마찬가지로 세 가지 버전이 필요하다.

Format 함수는 서식 조립을 하는데 printf함수처럼 %로 시작되는 서식을 사용하여 정수, 실수, 다른 문자열 등을 합친다. 이 함수는 가변 인수를 다룬다는 점에서 다른 함수들과는 조금 다른데 멤버 함수의 고유한 호출규약인 thiscall을 사용하지 않고 cdecl을 사용한다.

반응형

'책정리 > 혼자 연구하는 C,C++ 2' 카테고리의 다른 글

30장 다형성  (0) 2015.03.03
29장 상속  (0) 2015.02.28
27장 캡슐화  (0) 2015.02.20
26장 생성자  (0) 2015.02.20
25장 클래스  (0) 2015.02.20