본문 바로가기

C++

[TIL 26장] 연산자 오버로딩, 함수 객체, 함수 포인터, inline

오늘의 TIL 목차 (22. 09. 08)

 

  • 연산자 오버로딩(feat. 단항 연산자 오버로딩)
  • 함수 객체
  • inline

연산자 오버로딩


[ 연산자 오버로딩 ]

:  사용자 정의 타입(구조체, 클래스)의 자료형 간의 연산을 수행하기 위해 연산자의 이름을 이용한 함수를 만들어 주는 문법

class 클래스명
{
	// 연산자 종류 : +, -, *, /, ==, ++, --, etc.
	반환타입 operator+(매개변수) { 몸체코드; }
}

void main(void)
{
	클래스명 C;
    C + 매개변수; //  C.operator+(매개변수);와 동일
}

-

class CDainn()
{
public:
    int m_iA, m_iB;
    
    public:
    CDainn(int _iA, int _iB) : m_iA(0), m_iB(0); // 기본 생성자
    CDainn(int _iA, int _iB) : m_iA(_iA), m_iB(_iB); // 매개변수 생성자
    CDainn operator+(CDainn _class); // + 덧셈 연산 오버로딩 ( + 기능을 헤치면 안됨 )
}

CDainn& CDainn::operator+(CDainn _class){
	m_iA += _class.m_iA;
    m_iB += _class.m_iB;
    return *this;
}

void main(void)
{
	CDainn cd(10, 20);
    CDainn cdainn(1, 2);
    cd + cdainn; // cd.operator+(cdainn); 과 동일
    cout << cd.m_iA << ", " << cd.m_iB << endl; // public이라 접근 가능한거임 원래 안됨
}
출력 결과:
11, 22

 

 

■ 기능 / 주의

  • 연산자 오버로딩은 클래스 내에서 메소드로서만 사용 가능하다. (선언부, 정의부 따로 선언 가능)
  • 대입 연산자 ( = )의 경우 디폴트 연산자 (단순 1대1 대입)가 있다. ( 다른 연산자는 디폴트 존재 X )
  • 대입 연산자의 디폴트 연산자는 단순 1대1 대입을 하기 때문에 구조체나 포인터가 있을 경우 얕은 복사가 이뤄진다. 
    - 깊은 복사를 위한 대입 연산자를 명시적으로 선언해줘야 한다. ( 복사 생성자와 동일 )
  • 연산자 오버로딩은 매개변수를 하나만 받을 수 있다. (따라서 대부분 자기 자신과, 들어온 매개변수를 연산)
    - 매개변수 타입이 다르다면 함수 오버로딩 가능 ( ex. 객체와 int형 operator+ 도 가능 )
  • 연산자 오버로딩의 종류는 +, -, *, /, ==, ++, -- 등이 있다.
  • 연산자 오버로딩 시 복사 생성을 최소화하기 위해 레퍼런스를 주로 사용한다. ( 원본 참조 )

■ 예시

class CDainn
{
public:
	int m_iA, m_iB;

public:
	CDainn() : m_iA(0), m_iB(0) {} // 기본 생성자
	CDainn(int _iA, int _iB) : m_iA(_iA), m_iB(_iB) {} // 매개변수 생성자
	CDainn operator+(CDainn rhs); // + 덧셈 연산 오버로딩 ( + 기능을 헤치면 안됨 )
	// void operator+(CDainn rhs); // 반환타입만 다르기 때문에 함수 오버로딩 불가능

};

[ a + b 자체로 a의 값이 변하지 않도록 하기 위해서 CDainn타입으로 값만 반환 ]
CDainn CDainn::operator+(CDainn rhs) {
	CDainn Temp(m_iA + rhs.m_iA, m_iB + rhs.m_iB); // 두 객체를 더한 값으로 새로운 객체 생성
	return Temp; // 객체 반환, 지역변수라 임시니까 반환 시 다른 객체가 값을 받아줘야함
}

[ a += b 자체로 a의 값이 변하도록 CDainn& 반환하여 원본값 자체를 변경 ]
CDainn& CDainn::operator+=(CDainn rhs) {
	CDainn Temp(m_iA + rhs.m_iA, m_iB + rhs.m_iB); // 두 객체를 더한 값으로 새로운 객체 생성
	return Temp; // 객체 반환, 지역변수라 임시니까 반환 시 다른 객체가 값을 받아줘야함
}


* 자신의 멤버 변경 시 굳이 임시 객체 만들어서 반환하지 않고 void 타입으로 대입도 가능
* 다만 단항(++, --) 연산자 오버로딩 시 ++(++객체); 처럼 연속 연산을 원할 경우 문제 발생 
void CDainn::operator+(CDainn rhs) {
	m_iA += rhs.m_iA;
	m_iB += rhs.m_iB;
	cout << m_iA << ", " << m_iB << endl;
}

void main(void)
{
	CDainn cd1(10, 20);
	CDainn cd2(1, 2);
    
	// cd1.operator+(cd2)와 같음, Temp 객체 return
	// cd3 = Temp;와 같음 // 대입 연산자 (=)는 디폴트 (1대1 단순대입) 연산자가 있음 - 얕은 복사 주의
	CDainn cd3 = cd1 + cd2; 
	cout << cd3.m_iA << ", " << cd3.m_iB << endl; // public이라 접근 가능한거임 원래 안됨

	// 2. 함수 실행 시
	/*cout << cd1.m_iA; // 10
	cd1 + cd2; // cd1.operator+(cd2); 실행
	cout << cd1.m_iA;*/ // m_iA는 cd2.m_iA가 더해진 값인 11이 유지
}

-

// 클래스 CDainn이 존재할 때,
CDainn cd1 = CDainn(100, 'f'); // 오른쪽은 임시 객체

=> cd1.operator=(CDainn _rhs) { 몸체 }; 인 기본 디폴트 연산자가 실행됨
대입 디폴트 연산자 실행 후 복사 생성자 발생 ( 레퍼런스 붙이면 복사 생성자 발생 X 일듯 )

 

[ 단항 연산자 오버로딩 (전위 / 후의 연산) ]

[ 전위 연산 ] // CObj 클래스 내
CObj&		operator ++()	// 전위 연산
	{						// 선 연산 후 대입			
		m_iA += 1; 
		m_iB += 1; 

		return *this; // 증감된 값을 반환, 레퍼런스로 반환하여 바로 적용시킴
	}
[ 후위 연산 ]
CObj		operator ++ (int)		// 후위 연산, int는 단순 후위연산 표시 용도 ( 매개변수 넣는 거 아님 )
{									// 선 대입 후 연산
	CObj		Temp(*this);		// 복사 생성자 호출

	m_iA += 1;
	m_iB += 1;

	return Temp;
}
=> 똑같은 사본 Temp를 만든 이유는 후위 연산이 실행되는 해당 라인에선
증감되기 전의 값 Temp를 돌려주기 위해서이다. ( 후위연산은 다음 줄에서 증감이 되야하니까 )

void main(void) {
	cout << ((++obj2)++).m_iA << "\t" << endl; // 1
	cout << ((++obj2)++).m_iB << endl; // 3
	cout << (++(++obj2)).m_iC << endl; // 6
}
[ 전위 연산의 경우 void로 반환해도 멤버 값은 ++이 되는데 굳이 레퍼런스로 반환해준 이유 ]

void		operator ++()	// 전위 연산
{						// 선 연산 후 대입			
		m_iA += 1;
		m_iB += 1;

		return; // 증감된 값을 반환, 레퍼런스로 반환하여 바로 적용시킴
}

void main(void) {
	CObj cobj;
    ++cobj; 는 문제없이 가능하지만
    ++(++cobj); 또는 (++cobj)++; 시 반환값이 없어 연속 증감을 해내지 못한다.
    // (cobj++)++;인 연속 후위연산은 원래 불가능한 연산 방법임
}
  • 1. 전위 연산을 레퍼런스 타입으로 객체를 반환해야 하는 이유
  • 2. 후위 연산에 사본을 만들어주는 이유

 

함수 객체


[ 함수 객체 ]

:  클래스에 () 연산자를 오버로딩하여 객체를 함수처럼 사용하도록 만드는 문법

: STL에서 사용하는 조건자를 만드는 문법

class CDainn {
public:
	CDainn(int _iA) : m_iA(_iA) {} // 생성자
	반환타입 operator()(매개변수) { 몸체 } // 함수 객체 (=Functor)
private:
	int m_iA;
}

void main(void) {
	CDainn cd(10); // 생성자 실행
    cd(10); // cd.operator()(10);과 동일, 함수 객체 실행
}

-

 

 

[ 연산자 오버로딩과 함수 객체 비교 ]
* 연산자 오버로딩
1. operator뒤에 붙는 연산자(+, -, etc.)와 의도에 맞는 기능을 해야 한다.
2. 매개변수를 하나만 받을 수 있다.
3. 주로 자기자신과 매개변수의 연산을 담당한다.
4. 주로 자기 자신 또는 자신의 객체 타입을 반환한다.

* 함수 객체
1. 매개변수를 여러 개 받을 수 있다.
2. operator() 형태로 기능은 함수처럼 자유자재로 설정한다.
3. 반환타입도 함수의 기능에 따라 자유자재로 설정한다.

 

■ 기능 / 주의

  • 함수 객체는 클래스 내에 존재한다.
  • 함수 객체는 인라인 치환을 하여 알고리즘의 조건자로 주로 사용된다. ( 속도 빨라 효율 높음 )
    - 함수 객체는 inline 키워드를 명시하지 않아도 컴파일러가 자동으로 인라인 치환 처리 (콜백이 아닐 때)
  • 함수 객체는 클래스 안에 operator()을 선언하여 함수 모양처럼 사용하는 클래스이다. 
  • 다른 멤버 변수와 멤버 함수를 가질 수 있어서 일반 함수에서 할 수 없는 시도를 할 수 있다.
    - 주로 개별적으로 Functor라는 클래스를 만들어서 함수 객체를 만들어주는 듯하다.
  • 함수 객체는 인라인 함수 사용을 위해 쓰이는 것!

 

 

함수 포인터


[ 함수 포인터]

:  함수를 담는 포인터

반환타입 (*포인터명)(매개변수타입) = 받을함수;
int Sum(int _iA) { return _iA += _iA; )
int Minus(int _iA) { return _iA -= _iA; }

void main(void) {
	int (*funcptr)(int) = Sum; // 함수의 이름은 함수 주소, 주소를 받기
    funptr = Minus; // 함수 포인터 타입만 일치한다면 다른 함수 대입 받을 수 있음

 

 

■ 기능 / 주의

  • 함수 포인터는 반환 타입, 매개변수 타입이 같은 함수만 받을 수 있다.
  • 콜백 함수 등을 목적으로 사용했는데 지금은 함수 포인터를 거의 사용 안하는 듯하다.

 

inline


[ 인라인 ]

:  매크로처럼 함수 안의 코드가 치환되는 형태로 사용되는 것 ( 속도 빠름 )

-> 선언부에서 몸체까지 한번에 정의하면 코드(약 8줄 이내)에 따라 컴파일러가 알아서 inline화 (inline 앞에 선언 가능)

-> 함수 오버헤드가 발생하지 않아 속도가 빠른 대신 컴파일된 코드의 크기가 증가한다는 단점이 있음.

■ 기능 / 주의

  • 메모리 공간이 할당되지 않고 바로 치환되는 형태이다.
  • 함수 포인터를 사용하여 inline 함수의 이름을 초기화하는 경우 inline의 기능은 상실된다.
  • inline 함수를 재귀 형태로 호출할 때 inline의 기능을 상실한다.
    - 재귀 함수는 메모리 접근 횟수로 판단하는데 inline은 메모리 공간이 할당되지 않으므로 inline화가 풀림
  • 컴파일러는 재귀, 긴 함수, static, 반복문, switch문, goto문은 inline을 허용하지 않는다.