대입연산자의 오버로딩


대입연산자의 대표적인 특성


정의하지 않으면 디폴트 대입연산자가 삽입된다.

디폴트 대입연산자는 멤버 대 멤버의 복사 ( 얕은복사 ) 를 진행한다.

연산자 내에 동적할당을 한다면 , 그리고 깊은 복사가 필요하다면 , 직접 정의해야한다.


이와같이 복사생성자와 비슷한 특성을 가지는데 , 대표적인 차이는 복사생성자는 한 객체의 초기화에 기존에 생성된 객체를 사용할때 호출된다는것 이고 ,대입연산자는 두 객체가 생성및 초기화가 진행된상태일때 호출된다는것이다.


디폴트 대입 연산자의 문제점


연산자내에 동적할당시 , 얕은복사를 했을때 이미 소멸된 메모리를 또 delete 해서 오류가 발생하거나 가리키던 메모리를 해제하지않고 다른 메모리를 참조하여 메모리 누수가 발생하는등의 문제점이 생길 수 있다. 

따라서 생성자내에 동적할당을 하는경우 , 깊은복사를 진행하도록 정의하며 , 메모리 누수가 발생하지않도록, 깊은 복사에앞서 메모리해제의 과정을 거친다.


Person& operator=(const Person& ref)

{

delete name[]; // 메모리 누수를 막기위해 메모리해제

int len = strlen(ref.name)+1;

name = new char[len];

strcpy(name,ref.name);

age = ref.age;

return *this;

}


상속 구조에서의 대입 연산자 호출


유도클래스의 생성자에는 아무런 명시를 하지않아도 기초클래스의 생성자가 호출되지만, 유도클래스의 대입연산자에는 아무런 명시를 하지않으면 기초클래스의 대입연산자가 호출되지않는다. ( 디폴트 대입연산자는 기초클래스의 대입연산자까지 호출한다 ) 따라서 , 따로 명시해주어야한다.


Second& operator= (const Second& ref)

{

First::operator=(ref);

num3=ref.num3;

num4=ref.num4;

return * this;

}


이니셜라이저가 성능향상에 도움이되는이유


이니셜라이저만을 사용한 생성자일경우 , 기존의 객체를 이용하여 객체를 생성했을때 , 복사생성자만이 호출되지만, 

생성자의 몸체부분에서 대입연산을 통해 초기화하면 , 생성자와 대입연산자가 둘다 호출된다.

이는 이니셜라이저를 사용하면 선언과 동시에 초기화가 이루어지는 형태로 바이너리코드가 생성되지만  생성자의 몸체부분에서 대입연산을 통한 초기화를 진행하면 선언과 초기화를 별도의 문장에서 진행하는 형태로 바이너리코드가 생성되기때문이다.


 배열의 인덱스 연산자 오버로딩


C , C++의 기본배열은 경계검사를하지않아 arr[-1] 과 같은 엉뚱한 코드가 만들어질수있다. 이러한 단점을 배열클래스를 이용하여 해결할 수 있다.

[] 연산자를 오버로딩해야하는데 , 연산자의 기본특성상 멤버함수 기반으로만 오버로딩해야한다.

배열은 저장소의 일종이고 저장소에 저장돈 데이터는 유일성이 보장되어야하기때문에 , 저장소의 복사는 대부분 원천적으로 막는것이 좋다 . 

복사생성자와 대입연산자를 private 멤버로 둠으로서 복사와 대입을 막을수있다.


const 함수를 이용한 오버로딩의 활용


operator[]를 const 선언하면 배열을 멤버로 선언하는경우에는 저장 자체가 불가능해지기때문에 문제가 발생하고 , const 선언을 하지않으면 const 함수에서 호출시에 문제가 발생할때, const 함수를 오버로딩하면 이를 해결할 수 있다. ( cosnt 의 선언유무도 오버로딩에 포함된다 )


class BoundCheckIntArray

{

private : 

int * arr;

int  arrlen;

BoundCheckIntArray(const BoundCheckIntArray& arr) {} // 복사생성자를 private선언함으로써 복사를 제한

BoundCheckIntArray& operator= (const BoundCheckIntArray& arr) // 대입연산자를 private 선언함으로서 대입을 제한

public:

BoundCheckIntArray(int len) :arrlen(len)

{

arr = new int [arrlen];

}

int& operator[] (int idx)

{

if(idx<0 || idx>=arrlen) // 인수가 0보다작은지, 혹은 최대크기보다 큰지 확인

{

cout<<"Array index out of bound exception"<<endl;

exit(1);

}

return arr[idx];

}

int operator[] (int idx) const // const 오버로딩

{

if(idx<0 || idx>=arrlen)

{

cout<<"Array index out of bound exception"<<endl;

exit(1);

}

return arr[idx];

}

int GetArrLen () const 

{

return arrlen;

}

~BoundCheckIntArray()

{

delete[] arr;

}

};


void ShowAllData(const BoundCheckIntArray& ref) // [] 연산자를 const 오버로딩했기때문에 오류발생하지않음 

{

int len = ref.GetArrLen();

for(int idx =0; idx<len; idx++)

cout<<ref[idx]<<endl;

}


int main(void)

{

BoundCheckIntArray arr(5);

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

arr[i] = (i+1)*11; // [] 연산자 오버로딩을이용해 객체를 배열처럼활용

ShowAllData(arr);

return 0;

}


객체의 저장을 위한 배열클래스의 정의


저장의 대상이 객체일경우


Private:

Point * arr; // 객체의 배열

....

int main(void)

{

arr[0] = Point(3,4); // 임시객체를 생성해서 배열요소를 초기화 , 객체의 저장은 객체간의 대입여산을 기반으로한다.

arr[1] = Point(5,6);


return 0;

}


저장의 대상이 객체의 주소값인경우 ( 더 많이쓰임 )


typedef Point * POINT_PTR // Point * 를 POINT_PTR로 치환


private: 

POINT_PTR * arr; //주소값의 배열

...


int main (void)

{

arr[0] = new Point(3,4); // 객체의 주소값을 저장, 주소값을 저장할경우 깊은복사냐 얕은복사냐하는 문제를 신경쓰지않아도된다.

arr[1] = new Point(5,6);

delete arr[0];

delete arr[1];


return 0;

}


new delete 의 연산자 오버로딩


new 연산자가 하는일은


1. 메모리공간의 할당

2. 생성자의 호출

3. 할당하고자하는 자료형에 맞게 변환된 주소값의 형변환


오버로딩이 가능한부분은 1번에 해당하는 메모리공간의 할당부분뿐이다. 나머지는 컴파일러에의해 진행되며 , 수정할 수 없다.


void * operator new (size_t size) // new 오버로딩의 약속

{

void * adr = new char[size]; // char 의 메모리크기가 1바이트 이기때문에 char단위로 메모리공간을 할당

return adr; // 할당한 메모리주소의 반환

}

operator new 함수가 할당한 메모리공간의 주소갚을 반환하면, 컴파일러는 생성자를 호출해서 메모리공간을 대상으로 초기화를 진행하고 , 완성된 객체의 주소값을 클래스의 포인터형( 예시에서는 Point ) 으로 형변환하여 반환한다.


Point * ptr = new Point(3,4);


여기서 new 연산자가 반환하는값은 operator new 함수가 반환하는값이아니라 컴파일러에의해 적절히 형변환이 된 값이다.


delete 연산자 오버로딩


delete ptr;


이처럼 delete 연산자가 호출되면 컴파일러는 먼저 ptr 이 가리키는 객체의 소멸자를 호출하고 , 다음의형태로 정의된 함수에 ptr에 저장된 주소값을 전달한다. 소멸자는 오버로딩된 함수가 호출되기전에 호출이되므로 , 오버로딩된 함수에서는 메모리공간의 소멸을 책임져야한다.


void operator delete ( void * adr)

{       

delete [] adr; // 메모리공간 해제

}


new 연산자와 delete 연산자가 멤버함수임에도 불구하고 객체의 생성전에 호출 가능한이유


operator new 함수와 operator delete 함수는 static 함수 ( 전역함수 ) 이기때문이다 . 두함수는 따로 static 선언을 하지않아도 자동으로 static 함수로 간주된다.


new 연산자를 이용한 배열 할당


void * operator new (size_t size {}

void * operator new[] (size_t size) {}


이처럼 배열할당시 호출되는 함수라는 사실을 제외하고는 둘의 차이가없다. 이는 delete 연산자도 마찬가지다.


void operator delete (void  * adr) {}

void operator delete[] (void * adr){}


포인터 연산자의 오버로딩


-> 포인터가 가리키는 객체의 멤버에접근

* 포인터가 가리키는 객체에 접근


Number * operator->()

{

return this; // 객체의 주소값반환

}

Number& operator*()

{

return *this; // 객체 반환

}


스마트 포인터


스마트포인터는 포인터 역할을하는 객체를 뜻한다. 따라서 구현해야할 대상이다. 잘 정제되어진 라이브러리에서 제공하는 스마트포인터를 활용할 수 있다.


class SmartPtr

{

private:

Point * posptr;

public:

SmartPtr(Point * ptr) : posptr(ptr)

{}

Point& operator*() const

{

return *posptr;

}

Point * operator->() const

{

return posptr;

}

~smartptr()

{

delete posptr;

}

};


int main(void)

{

SmartPtr sptr(new Point(1,3));

sptr->SetPos(10,20);

return 0;

}


스마트포인터에서 가장 중요한사실은 Point객체의소멸을위한 delete연산이 자동으로 이루어진다는사실이다( smartptr 소멸자에서 delete연산을하기때문 )


() 연산자의 오버로딩과 펑터 (functor)


이연산자를 오버로딩하면 객체를 함수처럼 이용할 수 있게된다. 그리고 함수처럼 동작하는 클래스를 가리켜 펑터(functor) 라고하며 , 함수오브젝트라고도 불린다.

펑터는 객체의 동작방식에 유연함을 제공할때 주로사용된다.


class SortRule

{

public:

virtual bool operator()(int num1,int num2) const =0; // 추상클래스로선언 , 순수가상함수로 정의

};


class AscendingSort : public SortRule // 오름차순

{

public:

bool operator() (int num1, int num2) const

{

if(num1>num2)

return true;

else 

return false;

}

};


class DescendingSort : public SortRule // 내림차순

{

public:

bool operator() (int num1, int num2) const

{

if(num1>num2)

reutnr false;

else

return true;

}

};


int main (void)

{

....

void SortData( const sortRule& functor) // functor 에 DescendingSort 객체를넣거나 AscendingSort 객체를 넣어서 함수를 유연하게 사용할 수 있다

{

...

}

}


임시객체로의 자동 형변환과 형변환 연산자(Conversion Operator)


Number num;

num = 30; // 동일하지않은 자료형의 대입연산


이때 , num = Number(30); 이처럼 임시객체가 생성되고 , 대입연산자가 호출된다.


따라서 , A형 객체가 와야할 위치에 B형 데이터 ( 혹은 객체 ) 가 왔을경우 B 형 데이터를 인자로 전달받는 A 형클래스의 생성자호출을 통해서 A형 임시객체를 생성한다.



operator int() // 형변환연산자의 오버로딩 , 객체를 기본자료형으로 형변환 , 반환형을 명시하지않지만 return문에의한 값의반환은 가능하다.

{

return num;

}


Number num1 = 30; // 임시객체생성 , 대입연산자호출

Number num2 = num1+20; // 형변환연산자 호출 -> num1이 정수형데이터로 형변환



+ Recent posts