수업기록

클래스 문법의 대표적인 OOP는 다 배운 상황 그래서 generic으로 들어가기 전에 알아야할 필수 문법들을 배울 것임.

3개월 이후에 인프런 코드를 받게 되면 그때부터 자료구조 수업을 볼 사람을 볼 수 있도록 할 것. 그 외에는 전부 진행할 수 없으니 자주 사용할 것들만 배우고 넘어갈 것임.

큐 스택 리스트 - 선형적 자료 구조 정도는 개인적으로 공부를 하면 좋을 것이다. 그외의 학원에서 가르쳐주지 않는 자료 구조는 직접 필요한 상황에 있는 사람이 공부해라~


인라인과 연산자 오버로딩

인라인

인라인 함수라는 것이 있다! 원래 우리가 함수를 구현할 때에는 선언부/ 구현부를 나누어서 진행을 했었다. 함수를 호출하는 것은 함수를 의미하는 바이너리 코드를 메모리에 올려두고 필요할 때 불러오는 형태다. 그렇지만 작은 함수(가벼운)들은 함수를 호출하는 비용이 더 많은 비용을 소모할 수도 있다.

그렇기에 인라인 함수를 사용한다.

int Mul(int Dst, int Src){
	return Dst*Src;
}

인라인 함수는 함수의 선언과 동시에 구현이 진행되어야 함.

함수를 호출하는 것이 아닌, 매크로 함수처럼 그 위치에 코드를 옮겨 붙이는 형태로 진행한다.

inline int Mul(int Dst, int Src){
	return Dst*Src;
}

위와 같이 구현할 수 있음.

인라인의 구현 기준은 컴파일러가 똑똑해지면서, 컴파일러가 인라인을 붙이건, 붙이지 않건 스스로 판단하여 인라인을 구현하게 됨. (그래서 별로 신경 안써도 되는 문법이다.) 다만 현재 사용 용도는 프로그래머끼리의 의도를 명시적으로 보여주기 위하여 사용하는 것임.

우리가 배울 STL의 함수들은 인라인 함수로 구현되어 있는 것들이 다수 있음.

인라인 함수가 자동으로 함수로 변환되는 경우

  1. 함수 포인터에 인라인 함수의 이름을 저장할 경우1
  2. 인라인 함수를 재귀적으로 호출하게 될 경우 2

연산자_오버로딩 : 함수 오버로딩 규칙을 연산자에 적용하는 문법 #functor 사용시에 자주 사용할 수 있음 혹은 모던 C++에선 람다함수 로 대체할 수 있음.

연산자 좌측에 있는 객체 기준으로 동작한다.

C++의 객체가 없으면 사용할 수가 없다.

class CObj{
public: 
	CObj(int a, int b) : m_iA(a), m_iB(b){};
private:
	int m_iA;
	int m_iB;
}

int main(){
	CObj Dst(10,20);
	CObj Src(30,50);
	Dst = Dst + Src; //왼쪽 객체 기준(즉, Dst가 호출중)
}

위와 같은 상황에 대응하기 위한 문법. 직접 연산자와 같은 이름의 함수를 사용해서 원하는 기능을 직접 구현해보자!

operator : 연산자 형태의 이름을 가진 함수를 만드는 키워드

더하기 구현 예제

//CObj 클래스 내부
CObj operator + (const CObj& rhs){
	CObj Result (m_iA + rhs.m_iA, m_iB+rhs.m_iB);
	return Result;
}

오버로딩은 좌측의 객체를 기준으로 동작한다. 그래서 Dst + 40이 아니라 40 + Dst라면?

  1. 기본 오퍼레이터가 구현되어 있어야 한다
  2. 클래스 외부 전역 함수로 아래와 같이 구성한다.
CObj operator+(const CObj& lhs, const CObj& rhs) {
    return CObj(lhs.m_iA + rhs.m_iA, lhs.m_iB + rhs.m_iB);
}

클래스 멤버로만 구현 가능한 연산자 `= , () , [] ,

  1. 대입연산자 (오른쪽에서 왼쪽으로 움직임)
  2. 함수 호출연산자 (함수 이름은 왼쪽에 있음)
  3. 대괄호 연산자 (함수 이름은 왼쪽에 있음)
  4. 포인터 연산자 (함수 이름은 왼쪽에 있음)

디폴트 대입 연산자 !반환타입이 레퍼런스 Dst = Dst + Src; 자기 자신에게 대입하는 것이기 때문에

CObj& operator = (const CObj& rhs){
	m_iA += rhs.m_iA;
	m_iB += rhs.m_iB;
	return *this;
}

만약 멤버 함수 중에 포인터가 있다면, 그 포인터를 얕은 복사가 일어나지 않도록 고려해야 한다.


C언어 구조체 안에 string을 쓰면 안됨. 왜냐면 생성자와 같은 원리가 없기 때문에.

~ string은 대입연산자가 깊은 복사 형태로 동작함.

new와 delete도 연산자 오버로딩이 가능함. +) cout << 같은 것도 오버로딩 된 것임.


단항 연산자 오버로딩

전위 연산 : 반환타입은 자기 자신을 불러서 증가(선연산 후대입)

CObj& operator ++(){
	//내부 멤버 증가시키는 내용
	return *this;
}

후위 연산 : 반환타입은 객체 타입:: 복사본 (선 대입 후 연산)

CObj operator ++(int){
	CObj tmp(*this); //복사 생성자
	//this의 내부 멤버 증가시키는 내용
	return tmp;
}

매개 변수 안에 int는 후위연산을 알려주는 문법임. 후위연산은 복사본을 반환하기 떄문에 중첩이 되지 않음. (Dst++)++ 이거 원래도 안되는 거임.

연산자 오버로딩의 사용처! 보통 함수 객체!


functor : 함수 객체 - 객체를 함수처럼 사용하는 문법 () 소괄호 연산자를 오버로딩하여 사용. STL 알고리즘의 “조건자” 용도로 사용.

ex) 숫자를 정렬을 할 때, 오름차순으로 할 것인지? 내림차순으로 할 것인지? 이것을 결정할 때 사용할 수 있는 것이 조건자다. 이건 람다식으로 대체 가능 (성능은 람다식이 더 좋다.)

class Plus{
public:
	int operator()(int Dst, int Src){
		return Dst+Src
	}
}

int main(){
	Plus Functor;
	cout << Functor(10,20)<<endl;
}

객체를 함수처럼 사용하는 모습;

함수객체를 임시 객체화 시켰을 때 함수 호출하는 것보다 성능이 더 좋다 cout << Plus(10,20)<<endl; : 임시객체화

STL 조건자는 bool타입을 반환하는 형식의 함수 포인터를 사용하기 때문에 함수객체를 사용하면 내가 구현한 함수 객체를 들여보내서 구현할 수 있음.

class CSort{
public:
	virtual bool operator()(int Dst, int Src) =0
}

//오름차
class ASC : public CSort{
	bool operator()(int Dst, int Src) override{
		if(Dst > Srd) return true; //좌가 우보다 크면 바꾸고
		return false; //아니면 바꾸지 마라
	}
}

//내림차
class DSC : public CSort{
	bool operator()(int Dst, int Src) override{
		if(Dst > Srd) return false; //좌가 우보다 크면 바꾸고
		return true; //아니면 바꾸지 마라
	}
}
void BubbleSort(int pArrp[], int iSize, CSort& Functor){
	for(int i=0; i< iSize; ++i){
		for(int j=0; j<iSize-1; ++j){
			//조건자 역할
			if(Functor(pArr[j],pArr[i]+1)){
				swap(pArr[i],pArr[j]);
			}
		}
	}
}

위 두개를 엮어서 본다면, 함수객체를 조건자로 사용하는 것을 알 수 있다. 또한 불필요하게 스택 메모리를 사용하지 않도록 객체를 생성하지 말고 임시 객체를 사용해서 진행하는 것이 권장된다.

BubbleSort(배열,사이즈,DSC());

임시객체 : 이름 없이 임시 메모리 공간에 잠깐 할당되었다가 소멸되는 객체

근데 원래는 되는데, 컴파일러 업데이트와 모던 C++로 인해 임시 객체의 사용이 막히게 됨 만약 직접 사용하고 싶다면 R_Value_Reference 문법을 사용해야 함 (&&)

이전에 함수 포인터를 사용할 때에는 조건자로 함수를 넣게 되면 함수 포인터에 “함수 이름”을 넘겨주어야 하기 때문에 매개 변수를 함께 넘겨줄 수가 없었다. (함수 이름만 줘야 하니까.)

그러나 함수 객체는 생성자 오버로딩을 통해서 매개 변수를 함께 넘겨줄 수 있기 때문에 더욱 효율적이다. 그러나 람다 함수 이후로는 의미가 좀 퇴색되었지만…


cout의 동작 방식

# include <stdio.h>

namespace mystd{
	class Ostream{
	public :
		Ostream& operator <<(int iData){
			printf("%d,iData);
			return *this;
		} 
	}
}

요지는 우리가 사용하던 std::cout도 그저 클래스로 만들어져 있을 뿐이라는 것이고 <<연산자를 활용한 것이다. iostream도 마찬가지.

숙제 25-03-24 string 연산자 직접 구현


Footnotes

  1. 메모리 공간에 저장되어 있어야 하니까.

  2. 재귀함수의 경우 스택 프레임을 재구성해서 작동하게 되니까.