C++ STL - 연산자 오버로딩

seio·2022년 9월 8일
0

C++ STL

목록 보기
1/17

연산자 오버로딩(operator overloading)은 c++에서 제공하는 기본 타입이 아닌 사용자 정의 타입으로 연산자를 사용할 수 있게하는 문법이다

예를 들어 +의 연산자를 클래스의 연산자 오버로딩을 이용할 수 있다.

class point{
int x,y;
public:
point(~~); //생성자
const point operator+(point arg){
	point pt
	pt.x = this->x+arg.x;
	pt.y = this->y+arg.y;
	return pt;
}

int GetX() const{ // const 함수
	return x;
}
void SetX(int _x){ // 비 const 함수 : 멤버 변경
	x=_x;
}

};

int main(){

point p1,p2;

p1+p2;// p1.operator+(p2);와 같음

return 0;
}

const 멤버 함수

  • 멤버 함수 내에서 객체의 멤버 변수를 변경하지 않는다는 것을 보장하는 함수
  • const 객체는 const 멤버 함수만 호출할 수 있다.

따라서 아래 코드 처럼 const 객체가 비 const 함수를 호출할 경우 에러가 발생한다.

const point p1;

p1.SetX(99); // ERROR! const 객체는 const 멤버 함수만 호출가능

단항 연산자 오버로딩

단항 연산자는 ! & ~ * + - ++ -- 형 변환 연산자가 있다.

전위, 후위 연산자 오버로딩

++ 연산자는 전위, 후위 연산자가 있으며, operator++()와 operator++(int)와 같이 정의할 수 있다.
후위 연산자는 멤버 함수 호출시 의미없느(dummy) 정수형 인자를 전달합니다.

const point& operator++(){
	++x;
    ++y;
    return *this;
}
const point operator++(int){
	point pt;
    ++x; // 내부 구현이므로 전위 연산자 사용해도 무방
    ++y;
    return pt;
    
    OR
    
    point tmp = *this;
    ++*this;
    return tmp;
}

main(){
	point p1(2,3), p2(2,3);
    point result;
    
    result = ++p1;
    print (p1)
    print (result);
    
    result = p2++;
    print (p2)
    print (result);
}

출력
3,4
3,4
3,4
2,3

이항 연산자 오버로딩

이항 연산자는 +, -, *, /, ==, !=, <, <= 등이 있다.

bool operator==(point& arg) const{
return x==arg.x&&y==arg.y ? true: false;
}
bool operator!=(point& arg)const{
return !(*this==arg);
}

p1==p2 // p1.operator==(p2)와 같다.
p1!=p2 // p1.operator!=(p2)와 같다.

전역 함수를 이용한 연산자 오버로딩

연산자 오버로딩에는 2 가지가 있다.

1) 멤버 함수를 이용한 연산자 오버로딩
2) 전역 함수를 이용한 연산자 오버로딩

멤버 함수를 이용한 연산자 오버로딩을 사용할 수 없는 경우

  • 이항 연산의 왼쪽 항이 연산자 오버로딩 객체가 아니며 멤버 함수를 이용한 연산자 오버로딩을 이용할 수 없다. 이항 연산의 왼쪽 객체를 기준으로 연산자 오버로딩 멤버 함수를 호출하기 때문이다.

예를 들어

class point{...}

point p1, p2;

p1+p2; // p1.operator+(p2)와 같다.

k+p1; // Error, k는 연산자 오버로딩 객체가 아니므로 k.operator+(p1)를 호출 할수 없다.
      // operator+(k,p1)처럼 호출해야 한다.

연사자 오버로딩은 컴파일러가 p1==p2; 와 같은 코드를 두 가지로 해석한다.

1) 멤버 함수로 p1.operator==(p2); 처럼 해석하며, p1의 operator==() 멤버 함수를 호출해 p2를 인자로 전달한다.

2) 전역 함수로 operator==(p1,p2); 처럼 해석하며, 이것은 전역함수 operator==()의 인자로 p1, p2 객체를 각각 전달한다.

class point{...};

const point operator-(const point& p1, const point& p2){
	return point(p1.getx()-p2.getx(), p1.gety()-p2.gety());
}

int main(){

point p1,p2,p3;

p3= p1- p2;// operator-(p1, p2);

return 0;
}

전역 함수를 이용하면 point 클래스의 private 멤버인 x, y에 접근할 수 없으므로 getter를 이용하거나 프렌드 함수를 사용한다. 프렌드 함수는 아래와 같이 사용한다.

friend const point operator-(const point& p1, const point& p2){
   return ~~~;//operator-와 코드 동일 
}

프렌드 함수는 캡슐화를 저해하므로 가능하면 게터, 세터를 사용하는 방법이 좋다

프렌드 함수

  • 프렌드에는 함수 프렌드와 클래스 프렌드 두 가지가 있다.
    함수나 클래스를 프렌드로 지정하면 모든 클래스 멤버(private, protected, public)를 접근 제한 없이 사용할 수 있다.
@ 함수 프렌드 
class a{
 ...
 friend void Func();
 };
 void Func(){
 // class a의 모든 멤버를 접근 제한 없이 사용할 수 있다.
 }
@ 클랙스 프렌드
class a{
...
friend class b;
};
class b{
//class a의 모든 멤버를 접근 제한 없이 사용할 수 있다.
}

STL에 필요한 주요 연산자 오버로딩

함수 호출 연산자 오버로딩(() 연산자)

함수 호출 연산자 오버로딩은 객체를 함수처럼 동작하게 하는 연산자 입니다. c++에서 print(10)이라는 함수 호출 문장은 다음 세가지로 해석할 수 있다.

  • 1) 함수 호출: print가 함수 이름
  • 2) 함수 포인터: print가 함수 포인터
  • 3) 함수 객체: print가 함수 객체
struct Funcobject{
void operator()(int arg) const{};

};

void print1(int arg){};

int main(){
	void (*point2)(int) = print1;
    Funcobject print3;
    
    print1(10); // 1) '함수'를 사용한 정수 출력
    print2(10); // 2) '함수 포인터'를 사용한 정수 출력
    print3(10); // 3) '함수 객체'를 사용한 정수 출력 print3.operator(10)과 같음
    
}

함수 호출 연산자 오버로딩

	FuncObject print;
    1) print(1);
    2) print.operator()(1); 객체 생성 후 호출(명시적)
    3) FuncObject()(1); //임시 객체로 호출(암시적)
    4) FuncObject().operator()(1); // 임시 객체로 호출 (명시적)

메모리 접근, 클래스 멤버 접근 연산자 오버로딩

*, -> 연산자는 스마트 포인터나 반복자(iterator) 등의 특수한 객체에 사용된다.

스마트 포인터를 사용하여 &,*,->의 연산자 오버로딩을 한ㅏ. 스마트 포인터는 일반 포인터의 몇 가지 유용한 기능을 추가한 포인터처럼 동작하는 객체입니다.

일반 포인터의 사용은 아래와 같이 p1를 가르키는 동적 객체를 delete 연산으로 직접 제거해야 한다.

point* p1 =new point(1,2);
~~//동작
delete p1;

p1의 메모리 그림이다.

아래 코드는 pointptr 클래스의 소멸자를 이용해 동적으로 할당된 point 객체를 자동으로 제거한다.

class point{
int x, y;
public:
	point(int _x, int _y): x(_x),y(_y){}

};

class pointptr{
	point* ptr;
 
 public:
 pointptr(point *p):ptr(p){}
 ~pointptr(){
 	delete ptr;
 }
}

pointptr 클래스를 만들어 놓으면 동적으로 생성한 point 객체를 소멸자에서 자동으로 삭제하므로 프로그램 중에 예외가 발생하거나 delete 호출을 빼먹어 발생하는 동적 메모리 누수 현상을 방지할 수 있다.

그림은 p1의 메모리 그림이다.

p1은 스택 객체이므로 main() 함수 블록에서 제거되며 이때 p1의 소멸자에서 ptr이 가르키는 동적 메모리 객체를 제거한다.

p1이 일반 포인터처럼 동작하려면 p1로 point 클래스에 정의된 멤버 함수(서비스)를 사용할 수 있어야 한다. 이때 p1(pointptr 객체)로 point 클래스의 멤버를 접근할 수 있도록 -> 연산자를 오버로딩해야 한다.

class pointptr{
...
point* operator->()const{
	return ptr;
}

p1->print(); // p1.operator->()->print() 호출

point& operator*()const{
	return *ptr;
}
};

타입 변환 연산자 오버로딩

  • 1) 생성자를 이용한 타입 변환

  • 2) 타입 변환 연산자 오버로딩을 이용항 타입 변환

    생성자를 이용한 타입 변환

class A{};

class B{
	public:
    	B(){};
        B(a& _a){};
        B(int n){};
        B(double b){};
};

int main(){
	A a;
    int n = 10;
    double d = 5.5;
    
    B b; // B 생성자 호출
    b= a; // b= B(a) 암시적 생성자 호출 후 대입
    b= n; // b= B(n) 암시적 생성자 호출 후 대입
    b= d; // b= B(d) 암시적 생성자 호출 후 대입

}

point 객체에 정수를 대입하면 컴파일은 '가능하다' 이유는 정수를 인자로 받는 생성자가 있기 때문이다.


point(int _x=0,int _y=0):x(_x),y(_y){} // 생성자

void main(){
point pt;

pt = 10;// point(10,0) 암시적 생성자 호출
}

이렇게 하면 실수로 point 객체에 정수를 대입해도 컴파일이 성공하여 버그로 연결된다.

따라서 생성자를 이용한 형변환을 의도하지 않는다면 생성자는 명시적 호출만 가능하도록 explicit 키워드를 지정해야 한다.

explicit point(...):~~{}

main{
	point pt;
    pt = 10; //error 암시적 생성자 호출 불가능
    pt = point(10); // 명시적 생성자 호출만 가능
    
}

암시적인 생성자를 형변환을 의도하지 않는 한 "인자를 갖는 생성자는 모두 explicit 생성자로 만드는 것이 좋다".

타입 변환 연산자 오버로딩을 이용한 타입 변환

타입 변환 연산자는 생성자나 소멸자처럼 반환 타입을 지정하지 않는다.

class A{};
class B{

operator A(){ return A();}
operator int(){ return 10;}
operator double(){return 0.1;}
};

main(){
 A a;
 int n;
 double d;
 
 B b;
 a = b; // b.operator A() 
 n = b; // b.operator int()
 d = b; // b.operator double()
}
profile
personal study area

0개의 댓글