상수 객체 (const object)

C++에서 상수(const) 객체는 그 값이 변경될 수 없는 객체입니다.

상수 객체는 객체를 생성할 때 값이 할당되며, 이후에는 변경되지 않습니다.

 

const int num = 10; // 'num'은 상수 객체이며, 값은 10입니다.

위 드에서 const 키워드는 num 변수가 상수임을 나타냅니다. 이후에는 num의 값을 변경할 수 없습니다.

상수 객체는 객체 지향 프로그래밍에서 매우 중요한 역할을 합니다.

상수 객체를 사용하면, 값이 변경되면 안되는 상황에서 값이 변경되는 것을 방지할 수 있습니다.

또한, 상수 객체를 이용하면 컴파일러가 값을 임베딩(embedding)할 수 있어서, 코드의 실행 속도를 높일 수 있습니다.

 

코드의 실행 속도를 높일 수 있는 이유

상수 객체는 일반적으로 값을 변경할 수 없기 때문에, 컴파일러는 이러한 객체를 메모리에 상수로 저장하도록 최적화할 수 있습니다.

이를 통해 실행 속도를 향상시키고, 프로그램의 안정성을 높일 수 있습니다

 

   1. 메모리 접근 시간 단축

상수 객체의 값을 직접 코드에 삽입함으로써, 상수 객체의 값을 참조하기 위한 메모리 접근을 생략할 수 있습니다.

이는 프로그램 실행 시간을 단축시키는데 큰 도움이 됩니다.

 

const int num = 10;
int x = num + 5;

컴파일러는 num + 5를 15로 대체할 수 있습니다.

이렇게 상수 값을 바로 사용하면, 변수나 메모리를 참조하지 않고 값을 얻을 수 있으므로 실행 속도를 높일 수 있습니다.

 

    2.   코드 크기 감소

상수 객체를 사용하면, 상수 값이 중복해서 사용될 때 메모리에 상수 값의 복사본이 저장되는 것을 방지할 수 있습니다.

대신, 컴파일러는 상수 값을 임베딩해서 사용하므로 코드 크기가 감소하게 됩니다.

상수 값을 메모리에 저장할 경우, 상수 값이 여러 번 사용될 때마다 해당 메모리 주소를 참조하므로 코드가 길어질 수 있습니다.

하지만 상수 값을 임베딩하면 코드가 더 간결해지고, 프로그램의 크기가 작아질 수 있습니다.

결론적으로, 상수 객체를 이용하면 컴파일러가 값을 임베딩할 수 있어서, 코드의 실행 속도를 높일 수 있습니다.

이를 통해 프로그램의 성능을 향상시킬 수 있습니다.

 

 

C++에서 상수 객체는 전역 상수(constant)지역 상수(constant)로 나뉘어서 메모리의 서로 다른 영역에 저장됩니다.

전역 상수는 프로그램이 실행될 때 프로그램의 데이터 섹션(data section)에 저장됩니다.

데이터 섹션은 전역 변수와 정적(static) 변수도 저장하는 영역으로, 프로그램이 종료될 때까지 메모리에 남아있습니다.

지역 상수는 해당 변수가 정의된 블록이 실행될 때 스택(stack) 메모리 영역에 저장됩니다.

블록이 실행될 때마다 해당 상수의 값을 읽어와서 사용하며, 블록이 끝나면 해당 상수는 스택에서 제거됩니다.

 

상수 멤버함수 (const member function)

상수 멤버함수란 클래스 멤버함수 선언 뒤에 'const' 키워드로 선언 된 함수로,

상수 멤버 함수 안에서는 모든 멤버를 상수 처리한다는 특징이 있습니다.

여기서 상수 처리한다는 것은 함수 내부에서 사용되는 변수 값을 바꿀 수 없다는 의미입니다. 

 

#include <iostream>

class Point
{
private:
	int x, y;

public:	
	Point(int a = 0, int b= 0) : x(a), y(b) {}

	void set(int a, int b)
	{
		x= a;
		y= b;
	}
	void print() const // 상수 멤버 함수 : 안에서 모든 멤버를 상수처리
	{
		std::cout<< x << " , " << y <<std::endl;
	}
};

int main()
{
	const Point p(1,1);
	p.x = 10;     //error 값 바꿀 수 없음
	p.set(10,20); //error 값 바꿀 수 없음
	p.print(); // error

	return 0;
}

 

 위 코드에서 객체 p 는 const object 로 선언되었습니다. 따라서 내부 멤버 변수인 x 값을 직접 변경하거나, 변수 값을 변경하는 기능이 구현된 set함수를 호출할 경우 error 가 발생합니다.
그런데 print 함수는 값을 변경하지 않음에도 호출하지 못합니다.
그 이유는 p이 상수 객체이므로 호출하는 함수 또한 상수함수임이 보장되어야 하기 때문입니다.
이것을 보장하기 위해, 멤버함수를 const 선언 해줌으로써 컴파일러가 해당 함수가 변수 값을 변경하지 않는 const 함수라는 것을 알수 있도록 해주면 됩니다.

 

사실, 객체의 상태를 변경하지 않는 '모든' 멤버함수는 (getXXX 류), 반드시 const 멤버함수가 되어야 합니다.

#include <iostream>


struct Rect
{
	int ox, oy, width, height;

	Rect(int x= 0, int y= 0, int w= 0, int h = 0): ox(x), oy(y), width(w), height(h){}

	int getArea() const { return width*height;}
};


void foo(const Rect& r) // call by value overhaed --> const & 가 좋음
{
	int n = r.getArea();  
}
int main()
{

	Rect r(0,0, 10, 10);

	int n= r.getArea();

	foo(r);
	return 0;
}
위 예제는 getArea 함수가 const 선언 되어야 하는 이유에 대해 설명하는 코드 입니다.
foo()  라는 함수에서 Rect 의 인스턴스를 통해 getArea 함수를 호출하는 경우를 생각해봅시다.
이때 오버헤드 측면에서 call by value 형태로 인자를 받아오는 것보다는, const 객체 참조로 받는 것이 타당합니다.
과정에서 r이 '상수 객체'로서 복사(?) 되므로 getArea() 가 반드시 const 선언 되어있어야 r.getArea()를 호출 할 수 있게 됩니다.

 

논리적 상수성

Case1. mutable 이용

#include <iostream>
#include <stdio.h>

class Point
{
private:
	int x, y;
	// mutable 멤버 변수 : 상수 멤버함수 안에서도 값 변경 가능
	mutable char cache[16];
	mutable bool cache_valid = false;

public:	
	Point(int a = 0, int b= 0) : x(a), y(b) {}

	const char* toString() const// 상수 멤버함수여야 하는 이유
	{		
		if(cache_valid == false){
			sprintf(cache,"%d, %d", x,y );  // 멤버함수(cache, cache_valid를 변경할 수 없음)
			cache_valid = true;
		}
		return cache; 
	}
};
위 코드 예제는 sprintf 의 오버헤드가 크다고 가정하고,  cache 값이 변경됐을 경우에만 버퍼를 새롭게 써서 리턴하는 구조를 가져가는 형태로 구현을 한 것입니다.
그런데 toString 함수는 const 함수로 내부에서 멤버 변수들인 cache 및 cache_valid 값을 변경할 수 없습니다.
이 때, 이 두 변수들을 mutable 로 선언해주면, const 함수 내에서도 값을 변경할 수 있게 됩니다.
이렇게 함으로써 const 함수를 유지하면서도 내부적으로 변경이 필요한 변수들의 값을 바꿀 수 있습니다.

 

Case2. 포인터 이용

struct Cache
{
	char cache[16];
	bool cache_valid = false;
};
class Point
{
private:
	int x, y;
	Cache* pCache;
public:	
	Point(int a = 0, int b= 0) : x(a), y(b) {
		pCache = new Cache;
	}
	~Point(){
		delete pCache;
	}
	const char* toString() const// 상수 멤버함수여야 하는 이유
	{
 		if(pCache->cache_valid == false){
			sprintf(pCache->cache,"%d, %d", x,y );  // 멤버함수(cache, cache_valid를 변경할 수 없음)
			pCache->cache_valid = true;
		}
		return pCache->cache; 
	}
};
const 함수 내에 일부 변수를 변경하고자 할때 사용할 수 있는 두번째 방법은, 변경이 필요한 변수들을 별도 자료구조(Cache)에 담아두고, 이를 가리키는 포인터를 클래스 멤버 변수로 선언하는 것입니다.
이 객체가 생성자에서 Cache 인스턴스를 할당하고, const 함수 내에서는 이 인스턴스를 통해서 변수에 접근 및 변경을 할 수 있습니다.

 

const return

 

const를 반환하면, 반환된 값이 수정되지 않도록 보장합니다. 이는 다른 함수나 코드에서 반환된 값을 수정하는 것을 방지하므로 코드 안정성을 높이는 데 도움이 됩니다. 또한, 반환된 값을 const로 선언함으로써, 해당 값을 수정하는 코드가 컴파일러에서 에러로 처리되므로 유지 보수성을 높이는 데 도움이 됩니다.

또한, const를 반환하면 임시 객체에 대한 복사를 피할 수 있습니다. 예를 들어, 다음과 같은 함수가 있다고 가정해 봅시다.

 

const string getGreeting() {
  return "Hello";
}
이 함수가 호출되면, 문자열 "Hello"를 반환하는 임시 객체가 생성됩니다. 이 객체는 const로 반환되기 때문에, 함수 외부에서 수정될 수 없습니다. 만약 반환 값이 const가 아니라면, 임시 객체가 생성되는 것 외에도, 복사 생성자가 호출되어 복사본이 생성되어야 하므로 성능 저하를 초래할 수 있습니다.따라서, C++에서 const를 반환하는 것은 코드의 안정성, 유지 보수성, 성능 향상 등 여러 가지 이점을 제공합니다.

 

다른 예제를 살펴보겠습니다.

 

class Car {
public:
  const Engine& getEngine() const {
    return engine;
  }

private:
  Engine engine;
};
Car 클래스는  Engine 클래스의 객체를 포함하고 있습니다. 
getEngine() 함수는 const 로 선언되어 있으며, Engine 객체를 반환합니다.
이 함수는 const 로 선언되었기 때문에 Car 객체가 const 로 선언되었을 때 호출될 수 있습니다.

 

const Car myCar;
const Engine& myEngine = myCar.getEngine();
위 코드에서 myCar는 const로 선언되었으므로, getEngine() 함수는 const 버전이 호출됩니다. getEngine() 함수가 반환하는 Engine 객체는 const로 선언되어 있으므로, myEngine 변수도 const로 선언되어야 합니다.
이렇게 함으로써, myEngine 객체가 수정되지 않도록 보장됩니다.
만약 getEngine() 함수가 const를 반환하지 않는다면, myCar 객체가 const로 선언되었을 때 getEngine() 함수를 호출할 수 없으며, myEngine 변수를 const로 선언하지 않으면 myEngine 객체가 수정될 수 있습니다. 

 

따라서, const를 반환하는 함수를 사용함으로써 코드의 안정성과 유지 보수성을 높일 수 있습니다.

const pointer

 

C++에서 const 포인터는 포인터 자체가 변경될 수 없다는 것을 나타냅니다. 즉, 포인터가 가리키는 메모리의 값을 변경할 수는 있지만, 포인터가 가리키는 주소 자체는 변경할 수 없습니다.

const 포인터를 사용하면, 코드 안정성과 유지 보수성을 높일 수 있습니다. 예를 들어, 다음과 같은 코드를 생각해봅시다

 

void printValues(const int* values, int size) {
  for (int i = 0; i < size; ++i) {
    cout << values[i] << " ";
  }
}

 

위 코드에서 values 매개변수는 const int*로 선언되어 있습니다. 이는 values 포인터가 가리키는 메모리의 값을 변경할 수 있지만, values 포인터가 가리키는 주소 자체는 변경할 수 없다는 것을 나타냅니다.
이렇게 함으로써, printValues() 함수는 values 포인터를 통해 전달된 값들을 변경하지 않으므로, 호출한 쪽에서 전달한 값을 보존할 수 있습니다. 또한, const int* 타입을 사용하면, 잘못된 포인터 연산이나 잘못된 메모리 참조 등과 같은 문제를 방지할 수 있습니다.

 

또한, const 포인터는 포인터를 상수로 취급할 때도 유용합니다. 다음과 같은 예를 생각해보세요.

 

int value = 10;
const int* const pValue = &value;

 

위 코드에서 pValue는 const int* const로 선언되어 있습니다.

이는 pValue 포인터가 가리키는 주소 자체와, pValue 포인터가 가리키는 메모리의 값을 변경할 수 없다는 것을 나타냅니다.

이렇게 함으로써, pValue는 상수 포인터가 되어, 가리키는 값을 변경할 수 없으므로, 값이 변경되지 않도록 보장할 수 있습니다

 

 

'부담'과 관련된 표현들

 

#1. 부담 갖지 마세요

(부담갖지 말고 편하게 해도 된다 라고 전달하려고 할때)

 

No pressure

Don't worry about it

 

 

A: 죄송하지만 사진좀 찍어주실수 있으실까요?

B: 아..제가 똥손이라..

A: 오 부담갖지 마세요. 그냥 저희가 사진에 담기기만 하면돼요

 

A: Excuse me. Could you take a picture of us, please ?

B: Oh, I don't know. I take horrible pictures

A: Oh, no pressure/dont' worry about it. We just want a picture with all of us in it

 

#2. (괜히 나때문에) 부담드리고 싶지 않아요

(예를 들면, 타지로 여행갔는데 지인으로부터 호텔말고 자기 집에와서 묵으라고 제안받았을 때)

(내가 상대를 번거롭게 만드는 것 같을 때)

 

A: I don't want to bother you (방해하고 싶지 않다 라는 뉘앙스)

B: It's no bother

 

(더 적절한 표현)

A: I don't want to impose 

B: It's not an imposition

 

만약 그냥 거절하려면 (아니에요. 그건 아니죠)

No. I couldn't do that 

 

could : 그렇게 하고 싶어도 그럴 순 없지.

 

A: Why don't you stay for dinner ? please, stay for dinner

B : Oh, no. I couldn't do that 또는 I don't want to impose 

 

+ 만약 B 가 제안에 우물쭈물 고민하고 있을때, A가 부담갖지 마세요라는 표현으로 I don't wanna impose 라고 표현 가능

 


 
* 추가설명 :
그 외에 부담스럽다는 의미로 쓰이는 표현들 :
1. make <one> feel uncomfortable :
(누가 부담스럽게 쳐다 본다거나 부담스러운 질문을 할 때)
→ He made everyone at table very uncomfortable with his inappropriate questions.
 
2. breathe down <one's> neck (pressure와 비슷한 의미로 누구에게 압박을 준다는 의미에서 부담을 준다는 의미로 활용)
→ I can focus on my work a lot better without my boss constantly breathing down my neck!
 
3. You don't have to. / You can say no. → 안해도 돼 / 싫다고 해도 돼
(아주 편한 사이에 / 극히 캐쥬얼하게 표현하는 것이 더 적합한 상황에서)
 
* burden은 '부담'보다는 '짐'의 어감에 더 가깝게 쓰이는 경우가 많움
예) 가족에게 짐이 되고 싶지 않다 - I don't want to be a burden to my family.

고민에 대한  간접의문문 표현

어떻게 할지 (하는 것이 좋을지)

..할지 말지.. (하는 것이 좋을지 아닐지)

 

 

나 일찍 자는게 좋겠어

 

 I should go to bed early (일찍 자야돼라는 느낌보다는 일찍 자는게 좋겠어)

 

나 일찍자는게 좋을까? (의문문)

 

☞ Should I go to bed early ?

 

내가 일찍 자는 것이 좋을지 모르겠어 (간접의문문; ~ 하는 것이 좋을지 고민이야)

 

I'm not sure if i should go to bed early 

 

걔한테 이거에 대해 얘기 하는 것이 좋을지 고민이야 (잘 모르겠어)

 I'm not sure if i should tell him about this

 

이거에 대해서 걔한테 지금 얘기할지 아니면 기다릴지 고민이야

 I'm not sure if i should tell him about this now or wait

 

새 폰을 지금 살지 아니면 새로운 모델이 나올때까지 기다릴지 고민이야

 

 I'm not sure if i should buy a new smartphone now or wait until a new model comes out

  I'm trying to decide if i should buy a new smartphone now or wait until a new model comes out

(좀 더 직설적인 표현, 결정하려고 노력하고 있다. 즉, 고민중이다)

 

새 폰을 지금 살지 아니면 새로운 모델이 나올때까지 기다릴지 계속 고민중이야 ( 오랜시간 지속되어 오고 있다면 )

 

 I'm still trying to decide if i should buy a new smartphone now or wait until a new model comes out

 

걔한테 뭐라고 할까?

 

What should I tell him ?

 

걔한테 뭐라고 얘길 해줄지 고민이야

 

I'm not sure what I should tell him

 

어느 것을 사는 것이 좋을지 (아직) 고민이야

 

 I'm not sure which one I should buy

☞ I'm (still) tryting to decide which one I should buy

 


더 낫다 (it's better)

 

당신이 뭔가 잘못을 했으면, 그냥 그것을 인정하고 사과하는 것이 항상 낫(~하는게 낫다; 일반화)

 

If you did something wrong or made a mistake, it's always better to just admit it and apologize

 

A: 나 차 세차나 하러 갈까? 진짜 더럽네 (get car washed : 사역; 세차 맡기는 느낌)

B: 담주까지 기다리는게 나을듯 (일반화해서 말하는 것은 어색함). 이번 주 내내 비올거니까

B: 담주까지 기다리는게 나을수도 있을듯(좀 더 약하게).  이번 주 내내 비올거니까

 

 A: May be I should get my car washed. It's really dirty

 B: (I think) It would be better (for you) to wait unitl next week. because it's going to keep raining all week.

 B: (I think) It could/might be better (for you) to wait unitl next week. because it's going to keep raining all week.

 

*keep ~ing : 계속 ~ (한 상태) 하다

*all week/ this week : 이번 주 내내 

 

 

shared_ptr 을 사용하는 이유

 C++에서 메모리 누수(memory leak)를 방지하고, 안전하게 객체를 공유하는데 사용됩니다.

C++에서는 메모리를 수동으로 할당하고 해제하는 것이 일반적이지만, 이러한 작업을 관리하는 것은 어려울 수 있습니다.

특히, 동적으로 할당된 메모리를 사용하는 경우, 메모리를 해제하는 것을 잊어버리면 메모리 누수가 발생할 수 있습니다.

shared_ptr은 동적으로 할당된 메모리를 관리하는 스마트 포인터(Smart Pointer) 중 하나입니다.

스마트 포인터는 C++에서 메모리 관리를 자동화하는데 사용되며, 메모리 누수를 방지할 수 있습니다.

shared_ptr은 객체에 대한 공유 소유권(shared ownership)을 제공합니다.

이는 동일한 객체를 여러 개의 shared_ptr에서 공유할 수 있으며, 모든 shared_ptr이 삭제될 때 객체가 자동으로 해제된다는 것을 의미합니다.

이러한 공유 소유권을 제공하는 것은 객체에 대한 참조를 안전하게 공유하고, 객체가 더 이상 필요하지 않을 때 자동으로 삭제하여 메모리 누수를 방지하는 데 도움이 됩니다.

또한, shared_ptr은 일반적으로 new 및 delete 연산자를 사용하여 메모리를 동적으로 할당하고 해제하는 것보다 안전합니다. shared_ptr을 사용하면 자동으로 메모리가 해제되므로 메모리 누수 및 다른 문제가 발생할 가능성이 줄어듭니다.

 

shared_ptr 의 원리

shared_ptr은 객체에 대한 공유 소유권(shared ownership)을 제공하며, 모든 shared_ptr이 삭제될 때 객체가 자동으로 해제됩니다.

shared_ptr의 작동 방식은 참조 계수(reference counting)를 사용합니다.

즉, shared_ptr은 객체에 대한 참조를 가지고 있으며, 객체를 참조하는 shared_ptr의 개수를 계산합니다.

이러한 개수를 참조 계수라고 합니다. 참조 계수가 0이면 객체가 자동으로 해제됩니다.

객체를 생성할 때, shared_ptr은 객체와 함께 참조 계수를 생성합니다.

새로운 shared_ptr이 객체를 참조할 때마다, 참조 계수를 증가시킵니다.

shared_ptr이 객체에 대한 참조를 해제하면, 참조 계수를 감소시킵니다.

참조 계수가 0이 되면, shared_ptr이 객체를 삭제하고 참조 계수도 함께 삭제합니다.

shared_ptr은 여러 개의 shared_ptr에서 객체를 공유할 수 있습니다.

이때 shared_ptr은 동일한 참조 계수를 공유하며, 모든 shared_ptr이 삭제될 때 참조 계수가 0이 되어 객체가 자동으로 해제됩니다.

이러한 공유 소유권을 제공하는 것은 객체에 대한 참조를 안전하게 공유하고, 객체가 더 이상 필요하지 않을 때 자동으로 삭제하여 메모리 누수를 방지하는 데 도움이 됩니다.

shared_ptr은 std::make_shared 함수를 사용하여 생성할 수 있습니다.

이 함수는 동적으로 할당된 메모리를 자동으로 해제하므로, 메모리 누수와 관련된 문제를 방지할 수 있습니다.

예를 들어, 다음과 같은 코드를 사용하여 shared_ptr을 생성할 수 있습니다.

#include <memory>

int main() {
    // 객체를 생성하여 shared_ptr로 래핑합니다.
    std::shared_ptr<int> ptr1(new int(42));

    // make_shared 함수를 사용하여 shared_ptr을 생성합니다.
    std::shared_ptr<int> ptr2 = std::make_shared<int>(42);

    return 0;
}

코드에서 ptr1은 new 연산자를 사용하여 메모리를 동적으로 할당합니다.

반면에 ptr2는 std::make_shared 함수를 사용하여 메모리를 동적으로 할당합니다. 이러한 차이점은 메모리 누수 및 다른 문제를 방지하는 데 중요합니다

 

 

shared_ptr을 사용하여 객체를 공유하는 방법

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() {
        std::cout << "MyClass 생성자 호출" << std::endl;
    }
    ~MyClass() {
        std::cout << "MyClass 소멸자 호출" << std::endl;
    }
};

int main() {
    // MyClass 객체를 생성하여 shared_ptr로 래핑합니다.
    std::shared_ptr<MyClass> ptr1(new MyClass);

    // ptr1과 동일한 MyClass 객체를 참조하는 또 다른 shared_ptr을 만듭니다.
    std::shared_ptr<MyClass> ptr2(ptr1);

    // ptr1과 ptr2 모두가 객체를 참조하고 있습니다.
    std::cout << "ptr1.use_count(): " << ptr1.use_count() << std::endl;
    std::cout << "ptr2.use_count(): " << ptr2.use_count() << std::endl;

    // ptr1을 해제합니다.
    ptr1.reset();

    // ptr1이 해제되었으므로 ptr2만 객체를 참조하고 있습니다.
    std::cout << "ptr1이 해제되었으므로 ptr2만 객체를 참조합니다." << std::endl;
    std::cout << "ptr2.use_count(): " << ptr2.use_count() << std::endl;

    return 0;
}

위 코드에서 MyClass는 간단한 클래스로, 객체가 생성될 때 메시지를 출력하고 객체가 삭제될 때 메시지를 출력합니다.

main 함수에서는 MyClass 객체를 생성하여 shared_ptr로 래핑합니다.

그런 다음, ptr1과 동일한 객체를 참조하는 또 다른 shared_ptr인 ptr2를 만듭니다.

이후, ptr1과 ptr2의 use_count 함수를 호출하여 객체를 참조하는 shared_ptr의 개수를 확인합니다.

ptr1을 해제한 후 ptr2의 use_count를 다시 확인합니다. ptr1이 해제되었으므로 ptr2만 객체를 참조하고 있습니다.

이 예제는 shared_ptr이 객체에 대한 공유 소유권을 제공하는 방법을 보여줍니다.

또한, shared_ptr의 참조 계수를 확인하여 객체가 얼마나 많은 shared_ptr에서 참조되고 있는지 확인할 수 있습니다.

 

 

shared_ptr을 사용하여 복잡한 상호 참조 구조를 만드는 예제

#include <iostream>
#include <memory>

class B;
class C;

class A {
public:
    A() {
        std::cout << "A 생성자 호출" << std::endl;
    }
    ~A() {
        std::cout << "A 소멸자 호출" << std::endl;
    }
    void set_b(std::shared_ptr<B> b) {
        b_ = b;
    }
    void set_c(std::shared_ptr<C> c) {
        c_ = c;
    }
private:
    std::shared_ptr<B> b_;
    std::shared_ptr<C> c_;
};

class B {
public:
    B() {
        std::cout << "B 생성자 호출" << std::endl;
    }
    ~B() {
        std::cout << "B 소멸자 호출" << std::endl;
    }
    void set_a(std::shared_ptr<A> a) {
        a_ = a;
    }
    void set_c(std::shared_ptr<C> c) {
        c_ = c;
    }
private:
    std::shared_ptr<A> a_;
    std::shared_ptr<C> c_;
};

class C {
public:
    C() {
        std::cout << "C 생성자 호출" << std::endl;
    }
    ~C() {
        std::cout << "C 소멸자 호출" << std::endl;
    }
    void set_a(std::shared_ptr<A> a) {
        a_ = a;
    }
    void set_b(std::shared_ptr<B> b) {
        b_ = b;
    }
private:
    std::shared_ptr<A> a_;
    std::shared_ptr<B> b_;
};

int main() {
    std::shared_ptr<A> a(new A);
    std::shared_ptr<B> b(new B);
    std::shared_ptr<C> c(new C);

    a->set_b(b);
    a->set_c(c);

    b->set_a(a);
    b->set_c(c);

    c->set_a(a);
    c->set_b(b);

    std::cout << "a.use_count(): " << a.use_count() << std::endl;
    std::cout << "b.use_count(): " << b.use_count() << std::endl;
    std::cout << "c.use_count(): " << c.use_count() << std::endl;

    return 0;
}

 

 

다음 예제는 두 개의 클래스 Person과 Address를 정의하고, Person 객체가 Address 객체를 참조하도록 구현합니다.

Person 객체와 Address 객체를 모두 shared_ptr로 관리합니다

#include <iostream>
#include <memory>
#include <string>

class Address {
public:
    Address(const std::string& street, const std::string& city) : m_street(street), m_city(city) {
        std::cout << "Address 생성자 호출: " << m_street << ", " << m_city << std::endl;
    }
    ~Address() {
        std::cout << "Address 소멸자 호출: " << m_street << ", " << m_city << std::endl;
    }
    std::string getStreet() const {
        return m_street;
    }
    std::string getCity() const {
        return m_city;
    }
private:
    std::string m_street;
    std::string m_city;
};

class Person {
public:
    Person(const std::string& name, const std::string& street, const std::string& city) : m_name(name), m_address(std::make_shared<Address>(street, city)) {
        std::cout << "Person 생성자 호출: " << m_name << std::endl;
    }
    ~Person() {
        std::cout << "Person 소멸자 호출: " << m_name << std::endl;
    }
    std::string getName() const {
        return m_name;
    }
    std::shared_ptr<Address> getAddress() const {
        return m_address;
    }
private:
    std::string m_name;
    std::shared_ptr<Address> m_address;
};

int main() {
    // 두 개의 Person 객체를 생성합니다.
    std::shared_ptr<Person> person1(new Person("Alice", "123 Main St", "Anytown"));
    std::shared_ptr<Person> person2(new Person("Bob", "456 Elm St", "Anytown"));

    // person1과 person2가 참조하는 Address 객체를 출력합니다.
    std::cout << "person1이 참조하는 Address 객체: " << person1->getAddress()->getStreet() << ", " << person1->getAddress()->getCity() << std::endl;
    std::cout << "person2가 참조하는 Address 객체: " << person2->getAddress()->getStreet() << ", " << person2->getAddress()->getCity() << std::endl;

    // person1과 person2의 Address 객체가 같은지 확인합니다.
    if (person1->getAddress() == person2->getAddress()) {
        std::cout << "person1과 person2가 같은 Address 객체를 참조합니다." << std::endl;
    } else {
        std::cout << "person1과 person2가 다른 Address 객체를 참조합니다." << std::endl;
    }

    // person1을 삭제합니다.
    person1.reset();

    // person1이 참조하던 Address 객체를 출력합니다.
    if (person2->getAddress()) {
        std::cout << "person1이 참조하던 Address 객체: " << person2->getAddress()->getStreet() << ", " << person2->getAddress()->getCity() << std::endl;
    } else {
        std::cout << "person1이 참조하던 Address 객체

person1이 참조하던 Address 객체는 더 이상 참조되지 않으므로, 메모리에서 해제됩니다.

이 때, Address 클래스의 소멸자가 호출되고, 해당 객체가 속한 Person 객체도 메모리에서 해제됩니다.

person2는 여전히 Address 객체를 참조하고 있으므로, person2의 소멸자가 호출될 때까지 해당 Address 객체는 유효합니다.

shared_ptr은 복잡한 객체들을 관리하는 데 매우 편리한 기능을 제공합니다.

그러나 여러 shared_ptr이 하나의 객체를 참조하는 경우, 객체가 메모리에서 해제되는 시점이 예상과 다를 수 있으므로 주의해야 합니다. 또한, shared_ptr을 사용할 때는 서로 다른 스레드에서 동시에 접근하지 않도록 주의해야 합니다

 

 

게임 엔진에서 리소스 관리

 

게임 엔진에서 리소스 관리에 shared_ptr을 사용합니다.

게임 엔진에서는 이미지, 사운드, 모델 등 다양한 리소스를 로드하고 사용하는데, 이러한 리소스는 게임이 종료될 때까지 메모리에 유지되어야 하지만, 게임의 레벨이나 씬이 바뀔 때마다 리소스가 필요한 만큼만 로드하고, 사용이 끝나면 메모리에서 해제되어야 합니다.

 

shared_ptr은 리소스가 사용 중인지 아닌지를 추적할 수 있으므로, 여러 개의 객체에서 공유하며 사용하는 리소스를 안전하게 관리할 수 있습니다. 또한, shared_ptr은 객체가 더 이상 참조되지 않을 때 자동으로 메모리에서 해제되므로, 메모리 누수를 방지할 수 있습니다.

이러한 이유로, 많은 게임 엔진에서는 shared_ptr을 사용하여 리소스를 관리하고 있습니다. 예를 들어, Unreal Engine에서도 TSharedPtr이라는 이름으로 shared_ptr을 사용하여 객체를 관리합니다.

 

 

우선, Resource 클래스를 정의합니다.

이 클래스는 리소스의 기본 형태를 나타내며, 파일 이름과 크기를 멤버 변수로 가지고 있습니다.

class Resource {
public:
    Resource(const std::string& name, int size)
        : m_name(name), m_size(size) {}

    std::string getName() const { return m_name; }
    int getSize() const { return m_size; }

private:
    std::string m_name;
    int m_size;
};

 

다음으로, ResourceHolder 클래스를 정의합니다. 이 클래스는 리소스를 로드하고 관리하는 역할을 합니다.

ResourceHolder 클래스는 std::map을 사용하여 리소스를 이름으로 관리합니다.

class ResourceHolder {
public:
    std::shared_ptr<Resource> getResource(const std::string& name) {
        auto iter = m_resourceMap.find(name);
        if (iter != m_resourceMap.end()) {
            return iter->second;
        }
        else {
            // Load the resource from file
            std::shared_ptr<Resource> resource = std::make_shared<Resource>(name, 100);
            m_resourceMap[name] = resource;
            return resource;
        }
    }

private:
    std::map<std::string, std::shared_ptr<Resource>> m_resourceMap;
};

getResource 함수는 먼저 std::map에서 해당 이름의 리소스를 찾습니다. 이미 로드되어 있는 경우에는 해당 리소스에 대한 shared_ptr을 반환하고, 아직 로드되어 있지 않은 경우에는 파일에서 리소스를 로드하고 shared_ptr을 생성하여 std::map에 추가합니다.

 

마지막으로, main 함수에서 ResourceHolder 클래스를 사용하여 리소스를 로드합니다.

int main() {
    ResourceHolder resourceHolder;

    std::shared_ptr<Resource> res1 = resourceHolder.getResource("image1.png");
    std::shared_ptr<Resource> res2 = resourceHolder.getResource("sound1.wav");
    std::shared_ptr<Resource> res3 = resourceHolder.getResource("image1.png");

    std::cout << "Resource 1: " << res1->getName() << ", size: " << res1->getSize() << std::endl;
    std::cout << "Resource 2: " << res2->getName() << ", size: " << res2->getSize() << std::endl;
    std::cout << "Resource 3: " << res3->getName() << ", size: " << res3->getSize() << std::endl;
    std::cout << "res1 and res3 are " << (res1 == res3 ? "same" : "different") << std::endl;

    return 0;
}

main 함수에서는 ResourceHolder 클래스의 getResource 함수를 사용하여

image1.png와 sound1.wav 파일에서 리소스를 로드합니다.

image1.png 파일은 이미 한 번 로드되어 있기 때문에 getResource 함수는 새로운 shared_ptr을 생성하지 않고,

이미 로드된 리소스에 대한 shared_ptr을 반환합니다

 

 

주의사항

shared_ptr을 사용할 때 주의해야 할 몇 가지 내용이 있습니다.

  1. 순환 참조(circular reference) 문제: shared_ptr은 reference counting 방식을 사용하기 때문에 객체가 서로를 참조하는 경우 순환 참조 문제가 발생할 수 있습니다. 이런 경우에는 weak_ptr을 사용하거나, 참조 구조를 재설계해야 합니다.
  2. shared_ptr에 raw pointer를 대입하지 않기: shared_ptr에 raw pointer를 대입하게 되면, 두 개 이상의 shared_ptr이 같은 객체를 참조할 때, 객체가 제대로 해제되지 않는 문제가 발생할 수 있습니다. 대신, make_shared() 함수를 사용하거나 shared_ptr의 생성자에 raw pointer를 전달하는 것이 좋습니다.
  3. C++11에서는 std::enable_shared_from_this를 상속받아야 shared_ptr의 안전한 사용이 가능합니다. 이 클래스를 상속한 객체는 자신을 참조하는 shared_ptr을 생성할 수 있습니다.
  4. 사용자 정의 deleter를 지정할 수 있습니다. shared_ptr은 reference counting을 이용하여 객체를 관리하지만, 사용자가 직접 메모리 할당을 하고 해제해야 하는 경우가 있습니다. 이때 사용자 정의 deleter를 지정하여 메모리 할당과 해제를 처리할 수 있습니다.

 

 

 

 

  1. 리턴 값을 강제로 지정하는 예제

 

class MockFoo : public Foo {
 public:
  MOCK_METHOD(int, GetSize, (), (const, override));
};

TEST(MyMockTest, ReturnsTest) {
  MockFoo mock_foo;
  ON_CALL(mock_foo, GetSize()).WillByDefault(Return(42));
  EXPECT_EQ(42, mock_foo.GetSize());
}

 

2. 입력 인자를 확인하는 예제

class MockFoo : public Foo {
 public:
  MOCK_METHOD(int, Add, (int x, int y), (override));
};

TEST(MyMockTest, ArgumentTest) {
  MockFoo mock_foo;
  EXPECT_CALL(mock_foo, Add(4, 5)).WillOnce(Return(9));
  EXPECT_EQ(9, mock_foo.Add(4, 5));
}

 

3. 입력 인자를 변경하는 예제

class MockFoo : public Foo {
 public:
  MOCK_METHOD(int, Sum, (int x, int y), (override));
};

TEST(MyMockTest, ArgumentChangeTest) {
  MockFoo mock_foo;
  int x = 2;
  int y = 3;
  EXPECT_CALL(mock_foo, Sum(_, _)).WillOnce(DoAll(
      SaveArg<0>(&x), SaveArg<1>(&y),
      Return(0)));
  mock_foo.Sum(1, 2);
  EXPECT_EQ(1, x);
  EXPECT_EQ(2, y);
}

 

4. 순서를 지정하는 예제

 

class MockFoo : public Foo {
 public:
  MOCK_METHOD(int, GetSize, (), (const, override));
  MOCK_METHOD(void, SetSize, (int size), (override));
};

TEST(MyMockTest, OrderTest) {
  MockFoo mock_foo;
  EXPECT_CALL(mock_foo, GetSize()).WillOnce(Return(42));
  EXPECT_CALL(mock_foo, SetSize(84)).After(
      EXPECT_CALL(mock_foo, GetSize()).WillOnce(Return(42)));
  mock_foo.GetSize();
  mock_foo.SetSize(84);
}

 

5. 예외를 던지는 예제

 

class MockFoo : public Foo {
 public:
  MOCK_METHOD(void, Func, (), (override));
};

TEST(MyMockTest, ExceptionTest) {
  MockFoo mock_foo;
  EXPECT_CALL(mock_foo, Func()).WillOnce(
      Throw(std::runtime_error("error")));
  EXPECT_THROW(mock_foo.Func(), std::runtime_error);
}

6. 특정 횟수만큼 호출하는 예제

 

class MockFoo : public Foo {
 public:
  MOCK_METHOD(void, Func, (), (override));
};

TEST(MyMockTest, TimesTest) {
  MockFoo mock_foo;
  EXPECT_CALL(mock_foo, Func()).Times(2);
  mock_foo.Func();
  mock_foo.Func();
}

7. 다른 함수를 호출하는 예제

class MockFoo : public Foo {
 public:
  MOCK_METHOD(void, Func1, (), (override));
  MOCK_METHOD(void, Func2, (), (override));
};

TEST(MyMockTest, CallOtherFunctionTest) {
  MockFoo mock_foo;
  EXPECT_CALL(mock_foo, Func1())
      .WillOnce(InvokeWithoutArgs([&mock_foo]() {
        mock_foo.Func2();
      }));
  EXPECT_CALL(mock_foo, Func2());
  mock_foo.Func1();
}

 

 

gmock test 예제

  1. 함수 인자 검증 (Argument Verification)
TEST(MathTest, DivideByZero) {
  MockFunction<double(double, double)> mock_divide;

  EXPECT_CALL(mock_divide, Call(10, 0))
      .WillOnce(Throw(std::runtime_error("Divide by zero")));

  Math math;
  EXPECT_THROW(math.Divide(10, 0, &mock_divide), std::runtime_error);
}

 

이 예제에서는 Math 클래스의 Divide 함수에서 인자로 받은 두 숫자를 나누는데, 만약 두 번째 인자가 0이면 std::runtime_error 예외를 던지도록 되어 있음

이 함수가 제대로 작동하는지 검증하기 위해서, MockFunction을 이용해 인자로 10과 0을 전달했을 때 std::runtime_error 예외를 던지는 것을 확인

 

2. 다수의 반환값 (Multiple Return Values)

 

class MockDatabase : public Database {
 public:
  MOCK_METHOD(std::string, LookupName, (int id), (override));

 private:
  std::map<int, std::string> id_to_name_ = {
      {1, "Alice"},
      {2, "Bob"},
      {3, "Charlie"},
      {4, "Dave"},
      {5, "Eve"},
  };
};

TEST(LookupTest, ReturnsNameForValidId) {
  MockDatabase mock_database;
  EXPECT_CALL(mock_database, LookupName(1))
      .WillOnce(Return("Alice"));

  NameLookup name_lookup(&mock_database);
  std::string name = name_lookup.GetName(1);
  ASSERT_EQ("Alice", name);
}

 

이 예제에서는 Database 클래스를 상속받은 MockDatabase 클래스를 이용해 특정 id에 해당하는 이름을 반환하는 LookupName 함수를 테스트

EXPECT_CALL을 이용해 1에 해당하는 이름이 "Alice"인 것을 확인

 

3.  구글 테스트와 같이 사용하기 (Integration with Google Test)

TEST_F(MyTest, TestConnection) {
  MockConnection conn;
  ON_CALL(conn, Connect())
      .WillByDefault(Return(true));
  ON_CALL(conn, Disconnect())
      .WillByDefault(Return(true));

  MyObject obj(&conn);

  EXPECT_TRUE(obj.IsConnected());
  obj.DoSomething();
  EXPECT_TRUE(obj.IsConnected());
  obj.DoSomethingElse();
  EXPECT_TRUE(obj.IsConnected());
}

이 예제에서는 MyObject 클래스가 MockConnection을 이용해 연결하는지 여부를 검증

ON_CALL을 이용해 Connect()Disconnect() 함수가 호출될 때 각각 true를 반환하도록 설정한 후,

MyObject 객체를 생성하여 IsConnected() 함수의 반환값이 true인지 여부를 확인

이 예제는 구글 테스트(Google Test)와 함께 사용될 때 많이 쓰임

 

 

예제

#include <iostream>
#include <string>
#include <vector>
#include "gmock/gmock.h"

using namespace std;
using namespace testing;

// 모의 클래스 정의
class MyMockClass {
public:
    MOCK_METHOD2(MyMethod, int(int arg1, const string& arg2));
};

// 테스트 클래스 정의
class MyTestClass : public Test {
public:
    void SetUp() override {
        // 모의 클래스 객체 생성
        my_mock_object_ = new MyMockClass;
        // 모의 클래스 객체와 함수 연결
        ON_CALL(*my_mock_object_, MyMethod(_, _))
            .WillByDefault(Return(0));
    }

    void TearDown() override {
        delete my_mock_object_;
    }

protected:
    MyMockClass* my_mock_object_;
};

// 테스트 케이스 1
TEST_F(MyTestClass, Test1) {
    // 모의 함수 호출
    EXPECT_CALL(*my_mock_object_, MyMethod(42, "Hello"))
        .WillOnce(Return(1));
    // 테스트
    ASSERT_EQ(my_mock_object_->MyMethod(42, "Hello"), 1);
}

// 테스트 케이스 2
TEST_F(MyTestClass, Test2) {
    // 모의 함수 호출
    EXPECT_CALL(*my_mock_object_, MyMethod(69, "World"))
        .WillOnce(Return(2));
    // 테스트
    ASSERT_EQ(my_mock_object_->MyMethod(69, "World"), 2);
}

// 테스트 케이스 3
TEST_F(MyTestClass, Test3) {
    // 모의 함수 호출
    EXPECT_CALL(*my_mock_object_, MyMethod(123, _))
        .WillOnce(Return(3));
    // 테스트
    ASSERT_EQ(my_mock_object_->MyMethod(123, "Whatever"), 3);
}

// 테스트 케이스 4
TEST_F(MyTestClass, Test4) {
    // 모의 함수 호출
    vector<pair<int, string>> args = {{1, "a"}, {2, "b"}, {3, "c"}};
    vector<int> ret_vals = {4, 5, 6};
    EXPECT_CALL(*my_mock_object_, MyMethod(_, _))
        .WillOnce(Return(ret_vals[0]))
        .WillOnce(Return(ret_vals[1]))
        .WillOnce(Return(ret_vals[2]));
    // 테스트
    for (size_t i = 0; i < args.size(); ++i) {
        ASSERT_EQ(my_mock_object_->MyMethod(args[i].first, args[i].second), ret_vals[i]);
    }
}

// 테스트 케이스 5
TEST_F(MyTestClass, Test5) {
    // 모의 함수 호출
    EXPECT_CALL(*my_mock_object_, MyMethod(_, _))
        .WillOnce(Return(1));
    // 테스트
    ASSERT_EQ(my_mock_object_->MyMethod(42, "Hello"), 1);
    ASSERT_EQ(my_mock_object_->MyMethod(69, "World"), 1);
}

int main(int argc, char** argv) {
    InitGoogleMock(&argc, argv);
    return RUN_ALL_TESTS();
}


예제3

class Bank {
public:
    virtual ~Bank() {}
    virtual void deposit(int amount) = 0;
};
class Customer {
public:
    Customer(Bank* bank) : bank_(bank) {}

    void deposit(int amount) {
        if (amount <= 0) {
            throw std::runtime_error("Invalid amount");
        }
        bank_->deposit(amount);
    }

private:
    Bank* bank_;
};

Customer 클래스는 다음과 같이 Bank 클래스를 멤버로 가지고 있음

'

 

Customer 객체가 Bank 객체의 deposit 메서드를 호출할 때, 다양한 오류 상황이 발생할 수 있음

이를 gmock을 활용해서 검증

 

먼저, Bank 클래스를 mocking

class MockBank : public Bank {
public:
    MOCK_METHOD(void, deposit, (int amount), (override));
};

 

다음으로, Customer 클래스의 deposit 메서드가 예상한 대로 작동하는지 검증해봅니다. 이때, Bank 클래스의 deposit 메서드를 호출할 때, 어떤 인자가 전달되는지에 대한 검증도 수행

 

TEST(CustomerTest, DepositSuccess) {
    MockBank bank;
    Customer customer(&bank);

    EXPECT_CALL(bank, deposit(100));

    customer.deposit(100);
}

 

이번에는 Customer 객체가 deposit 메서드를 호출할 때, 음수 값을 전달하는 경우가 발생하는지 검증해보겠습니다. 이를 위해서는 Bank 객체의 deposit 메서드가 호출되지 않아야 합니다

 

TEST(CustomerTest, DepositWithNegativeAmount) {
    MockBank bank;
    Customer customer(&bank);

    EXPECT_THROW(customer.deposit(-100), std::runtime_error);
    EXPECT_CALL(bank, deposit(_)).Times(0);
}

 

마지막으로, Bank 객체의 deposit 메서드에서 예외가 발생하는 경우도 검증해봅니다. 이를 위해서는 Customer 객체가 Bank 객체의 deposit 메서드를 호출할 때, 예외가 발생할 것임을 미리 알고 있어야 합니다.

 

TEST(CustomerTest, DepositThrowsException) {
    MockBank bank;
    Customer customer(&bank);

    EXPECT_CALL(bank, deposit(100)).WillOnce(
        [] (int amount) { throw std::runtime_error("Failed to deposit"); });

    EXPECT_THROW(customer.deposit(100), std::runtime_error);
}

위 예제에서는 gmock을 활용해서 Customer 객체가 Bank 객체의 deposit 메서드를 호출할 때, 발생할 수 있는 다양한 오류 상황을 검증

 

 

설명

 

  • 어떤 프로그램이 Customer 객체를 생성합니다.
  • Customer 객체는 Bank 객체에 대한 참조자를 가지고 있습니다.
  • Customer 객체의 makeDeposit 메서드를 호출하면, Bank 객체의 deposit 메서드가 호출되어야 합니다.
  • Bank 객체의 deposit 메서드는 인자로 받은 금액을 해당 계좌에 입금합니다.
  • 이때, Bank 객체가 인증되지 않은 경우에는 입금이 되지 않아야 합니다.

위의 상황을 검증하기 위해서는 다음과 같은 동작이 필요합니다.

  1. Bank 객체를 모의(mock)합니다.
  2. Bank 객체를 모의하면서, deposit 메서드가 호출될 때 인자로 넘어오는 값을 검증할 수 있도록 합니다.
  3. Bank 객체를 모의하면서, deposit 메서드가 호출되기 전에 먼저 인증 과정을 거쳐야 한다는 것을 검증할 수 있도록 합니다.
  4. Customer 객체를 생성합니다. 이때, 모의된 Bank 객체의 참조자를 Customer 객체에 전달합니다.
  5. Customer 객체의 makeDeposit 메서드를 호출합니다. 이때, 모의된 Bank 객체의 deposit 메서드가 호출되어야 합니다.
  6. 모의된 Bank 객체에서 deposit 메서드의 인자로 넘어온 값을 검증합니다.
  7. 모의된 Bank 객체에서 인증 과정이 올바르게 진행되었는지 검증합니다.

이렇게 하면, Customer 객체가 Bank 객체의 deposit 메서드를 호출할 때 발생할 수 있는 다양한 오류 상황을 검증할 수 있습니다. 이를 위해서 gmock을 사용하면, 모의 객체를 쉽게 생성하고 다양한 동작을 검증할 수 있습니

'유닛 테스트(unit test)' 카테고리의 다른 글

[C++] google test - gmock #1  (0) 2023.03.24

unit test 란

 

 

 

mocking 이란

mocking은 유닛 테스트에서 사용되는 기술 중 하나로, 의존성 있는 코드를 테스트할 때, 해당 의존성을 가짜 객체로 대체하여 테스트하는 것을 의미함

즉, 의존성 있는 코드를 호출할 때, 그 코드가 호출하는 객체를 대신해서 가짜 객체를 사용하여 호출하는 것

이 가짜 객체는 실제 객체와 동일한 인터페이스를 제공하지만, 미리 정의된 테스트 케이스에 따라서 적절한 결과를 반환

이렇게 하면 의존성이 있는 코드의 결과를 예측 가능하게 만들어

또한, 의존성이 있는 코드를 모의(mock) 객체로 대체하면, 해당 객체가 실제 시스템에 영향을 미치지 않고도 코드를 테스트할 수 있음

 

 

Queue라는 클래스가 있다고 가정하고, Queue가 의존하는 클래스인 Data라는 클래스를 mocking하는 예제

// Queue.h
#include "Data.h"

class Queue {
public:
    Queue(Data* data);
    int size();
private:
    Data* data_;
};

// Queue.cpp
#include "Queue.h"

Queue::Queue(Data* data) : data_(data) {}

int Queue::size() {
    return data_->getSize();
}

// MockData.h
#include "Data.h"
#include "gmock/gmock.h"

class MockData : public Data {
public:
    MOCK_METHOD(int, getSize, (), (override));
};

// QueueTest.cpp
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "Queue.h"
#include "MockData.h"

using ::testing::Return;

TEST(QueueTest, SizeTest) {
    MockData mock_data;
    Queue q(&mock_data);
    EXPECT_CALL(mock_data, getSize()).WillOnce(Return(3));
    ASSERT_EQ(q.size(), 3);
}

 

이 예제에서는 Queue 클래스가 Data 클래스에 의존하고 있음

그리고 QueueTest에서는 Queue의 size() 메소드를 테스트하고자 함

하지만, size() 메소드는 Data의 getSize() 메소드를 호출하기 때문에, Data 클래스에 의존하고 있는 상황

이를 해결하기 위해 MockData 클래스를 만들어 Data 클래스를 mocking

 

Queue 클래스에서는 생성자에서 Data 객체를 받아서 멤버 변수로 저장

size() 메소드에서는 Data 객체의 getSize() 메소드를 호출하여 반환값을 리턴

QueueTest에서는 MockData 객체를 생성하고, Queue 객체를 생성할 때 이를 전달

그리고 EXPECT_CALL 매크로를 사용하여, MockData 객체의 getSize() 메소드가 호출될 때 3을 반환하도록 지정

마지막으로 ASSERT_EQ 매크로를 사용하여 Queue 객체의 size() 메소드가 3을 반환하는지 확인

이렇게 Mocking을 통해, Queue 클래스가 Data 클래스에 의존하는 상황에서도 테스트를 수행할 수 있게됨

 

 

mocking을 사용하여 실제 데이터베이스에 접근하지 않고 데이터베이스를 대체하는 예제

 

예를 들어, 데이터베이스에서 사용자 정보를 가져와 로그인 인증을 수행하는 함수가 있는데,

이 함수는 데이터베이스에 직접 연결하여 쿼리를 실행하므로 테스트를 수행하는 데 시간이 많이 소요된다고 가정

이런 경우 mocking을 사용하여 데이터베이스에 연결하지 않고도 테스트를 수행할 수 있음

 

먼저 사용자 정보를 나타내는 User 클래스는 아래 처럼 디자인 해볼 수 있음

class User {
public:
    User(const std::string& name, const std::string& password) : name_(name), password_(password) {}
    const std::string& GetName() const { return name_; }
    const std::string& GetPassword() const { return password_; }
private:
    std::string name_;
    std::string password_;
};

 

데이터베이스와 상호작용하는 UserRepository 클래스

class UserRepository {
public:
    std::shared_ptr<User> GetUserByName(const std::string& name) {
        // 데이터베이스에 연결하여 쿼리 실행
        // ...
        // 결과를 User 객체로 변환하여 반환
        return std::make_shared<User>(name, "password");
    }
};
 
UserRepository를 사용하여 로그인 인증을 수행하는 LoginService 클래스
 
class LoginService {
public:
    bool Authenticate(const std::string& name, const std::string& password) {
        UserRepository repository;
        std::shared_ptr<User> user = repository.GetUserByName(name);
        if (!user) {
            return false;
        }
        return user->GetPassword() == password;
    }
};

 

이제 LoginService의 Authenticate 함수를 테스트하려면 UserRepository 클래스의 GetUserByName 함수를 호출해야 함

그러나 이 함수는 데이터베이스에 직접 연결하기 때문에 테스트하는 데 시간이 많이 소요됨

따라서 mocking을 사용하여 UserRepository 클래스의 GetUserByName 함수를 대체할 수 있음

 

이를 위해 UserRepository 클래스를 인터페이스로 추상화하고, 이 인터페이스를 상속하는 MockUserRepository 클래스를 작성

class IUserRepository {
public:
    virtual std::shared_ptr<User> GetUserByName(const std::string& name) = 0;
};

class MockUserRepository : public IUserRepository {
public:
    MOCK_METHOD1(GetUserByName, std::shared_ptr<User>(const std::string& name));
};

 

여기서 MOCK_METHOD1 매크로는 IUserRepository 클래스의 GetUserByName 함수를 대체하기 위한 mocking 함수를 생성

이제 LoginService 클래스의 Authenticate 함수를 테스트하려면 MockUserRepository 클래스의 GetUserByName 함수를 호출

 

TEST(LoginServiceTest, TestAuthenticate) {
    // Mocking을 사용하여 UserRepository 대체
    MockUserRepository repository;
    LoginService service;

    // mocking 함수 설정
    EXPECT_CALL(repository, GetUserByName("Alice"))

 

 

  1. Mocking with multiple dependencies (다수의 의존성을 가진 클래스에 대해 mocking을 수행하는 예제)

아래 코드에서는 ClassA와 ClassB 두 개의 클래스가 ClassC에서 의존성으로 사용되며,

ClassC의 테스트를 위해 ClassA와 ClassB를 mocking 

class ClassA {
public:
  virtual void funcA() = 0;
};

class ClassB {
public:
  virtual void funcB() = 0;
};

class ClassC {
public:
  ClassC(std::shared_ptr<ClassA> a, std::shared_ptr<ClassB> b) 
    : a_(a), b_(b) {}

  void funcC() {
    a_->funcA();
    b_->funcB();
  }

private:
  std::shared_ptr<ClassA> a_;
  std::shared_ptr<ClassB> b_;
};

TEST(ClassCTest, TestFuncC) {
  // Mock ClassA and ClassB
  auto mockA = std::make_shared<MockClassA>();
  auto mockB = std::make_shared<MockClassB>();

  // Set expectations
  EXPECT_CALL(*mockA, funcA());
  EXPECT_CALL(*mockB, funcB());

  // Inject mocks into ClassC
  ClassC c(mockA, mockB);

  // Test ClassC's funcC
  c.funcC();
}

 

 

2. Mocking with non-virtual interface (가상 함수가 아닌 인터페이스를 사용하는 클래스에 대해 mocking을 수행하는 예제)

 

아래 코드에서는 ClassD가 ClassE를 의존성으로 사용하며, ClassD를 mocking

ClassD는 인터페이스인 ID를 구현하며, ClassE는 ID를 통해 ClassD와 상호작용함

class ID {
public:
  virtual void funcD() = 0;
};

class ClassD : public ID {
public:
  void funcD() override {
    funcE();
  }

  virtual void funcE() {
    // implementation
  }
};

class ClassE {
public:
  ClassE(ID& d) : d_(d) {}

  void funcE() {
    d_.funcD();
  }

private:
  ID& d_;
};

class MockClassD : public ID {
public:
  MOCK_METHOD(void, funcD, (), (override));
};

TEST(ClassETest, TestFuncE) {
  // Mock ClassD
  MockClassD mockD;

  // Set expectations
  EXPECT_CALL(mockD, funcD());

  // Create ClassE with mockD
  ClassE e(mockD);

  // Test ClassE's funcE
  e.funcE();
}

 

 

3. Partial Mocking (Partial Mocking을 사용하여 클래스의 일부 함수만 mocking 하는 예제)

아래 코드에서는 ClassF가 funcF와 funcG 함수를 가지며, funcF를 mocking 

funcG 함수는 실제로 호출되며, ClassF의 인스턴스는 Partial Mocking이 적용

 

class ClassF {
public:
  virtual void funcF() {
    // implementation
  }

  void funcG() {
    // implementation
  }
};

TEST(ClassFTest, TestFuncF) {

 

 

+ QNX 에서  WillOnce 사용 불가.

 

이 경우에는 대신 OnCall 함수를 사용하여 함수 호출 시점에서 특정 값을 반환하도록 설정

 

class MyClass {
public:
    virtual int GetValue() const { return 0; }
};

class MyMock : public MyClass {
public:
    MOCK_CONST_METHOD0(GetValue, int());
};

 

MyMock 클래스에서 GetValue() 함수를 mock으로 설정하고,

WillOnce 함수를 사용하여 값을 반환하는 것 대신,

OnCall 함수를 사용하여 특정 값을 반환하도록 설정할 수 있습니다.

 

즉  다음과 같은 방법으로 설정할 수 있음

MyMock mock;
EXPECT_CALL(mock, GetValue())
    .WillOnce(Invoke([&]() {
        // OnCall 대신에 직접 처리하면 됩니다.
        return 42;
    }));

이제 MyMockGetValue() 함수가 호출되면, 42를 반환하도록 설정되어 있음

이렇게 하면 WillOnce 함수를 사용할 수 없는 환경에서도 유용한 방법으로 mocking을 구현할 수 있음

 

 

예제2

#include <iostream>
#include <memory>

class IDataReader {
public:
    virtual ~IDataReader() = default;
    virtual int ReadData(char* buffer, int size) = 0;
};

class MockDataReader : public IDataReader {
public:
    MOCK_METHOD(int, ReadData, (char* buffer, int size), (override));
};

class DataProcessor {
public:
    explicit DataProcessor(std::shared_ptr<IDataReader> reader) : reader_(std::move(reader)) {}

    bool ProcessData() {
        char buffer[1024];
        int bytes_read = reader_->ReadData(buffer, sizeof(buffer));

        if (bytes_read < 0) {
            std::cerr << "Error reading data" << std::endl;
            return false;
        }

        // process the data...

        return true;
    }

private:
    std::shared_ptr<IDataReader> reader_;
};

class MockDataReaderWrapper : public IDataReader {
public:
    MockDataReaderWrapper(MockDataReader& mock_reader) : mock_reader_(mock_reader) {}

    int ReadData(char* buffer, int size) override {
        return mock_reader_.ReadData(buffer, size);
    }

private:
    MockDataReader& mock_reader_;
};

TEST(DataProcessorTest, ProcessDataTest) {
    MockDataReader mock_reader;
    MockDataReaderWrapper mock_reader_wrapper(mock_reader);
    DataProcessor processor(std::make_shared<MockDataReaderWrapper>(mock_reader_wrapper));

    EXPECT_CALL(mock_reader, ReadData(_, _)).WillOnce(Return(10));

    ASSERT_TRUE(processor.ProcessData());
}

이 예제에서는 IDataReader라는 인터페이스를 정의하고, 이를 구현하는 MockDataReader 클래스를 만들었음

그리고 DataProcessor 클래스는 IDataReader 객체를 인자로 받아 데이터를 처리함

 

위 예제에서는 WillOnce 함수 대신 MockDataReaderWrapper 클래스를 만들어 사용

MockDataReaderWrapper 클래스는 IDataReader 인터페이스를 구현하면서, 내부적으로 MockDataReader 객체를 참조 DataProcessor 클래스에서는 std::shared_ptr<IDataReader> 타입으로 MockDataReaderWrapper 객체를 받아 처리

마지막으로 EXPECT_CALL 매크로를 사용하여 MockDataReader 클래스의 ReadData 함수가 호출될 때 반환 값을 지정

이 때 ReadData 함수는 MockDataReaderWrapper 클래스를 통해 호출됨

이 예제에서는 MockDataReaderWrapper 클래스를 사용하여 QNX 환경에서 WillOnce 함수를 대체할 수 있음

 

 

'유닛 테스트(unit test)' 카테고리의 다른 글

[C++] google test - gmock #2  (0) 2023.03.25

C++에서 가상 함수(Virtual function)은 다형성을 구현하는데 매우 중요한 역할을 함

가상 함수는 일반적인 멤버 함수와 달리, 함수 호출시 객체의 실제 타입을 기반으로 호출할 함수를 동적으로 바인딩

이를 통해 런타임 다형성을 지원하며, 객체의 동작을 유연하게 변경할 수 있음

 

가상 함수를 사용하면 다형성을 지원하는 클래스의 인스턴스들이 메모리 상에서 어떻게 구성되는지를 관리하는 Virtual table이 생성됨

 

가상 테이블은 가상 함수의 포인터를 저장하고, 객체의 실제 타입에 맞는 가상 함수를 호출

이렇게 가상 함수와 가상 테이블을 사용하여 다형성을 구현함으로써, OOP 특징인 추상화, 캡슐화, 상속, 다형성 등 특징을 활용할 수 있음

 

 

가상 테이블은 클래스의 정의가 컴파일 될 때 만들어지며, 각 클래스당 하나씩 존재

 

가상 테이블은 각 객체에게 독립적으로 할당되며, 객체의 가상 함수 호출은 해당 객체의 가상 함수 테이블을 통해 이루어짐

객체가 생성될 때, 가상 함수 포인터 테이블이 생성되며, 이 테이블에는 해당 객체의 가상 함수들에 대한 포인터들이 저장됨

상속 관계에서 파생 클래스의 가상 함수 테이블은 기본 클래스의 가상 함수 테이블을 상속받음

만약 파생 클래스에서 기본 클래스의 가상 함수를 재정의(오버라이딩)하면, 해당 함수에 대한 포인터가 파생 클래스의 가상 함수 테이블에 저장됨

이렇게 하위 클래스에서 재정의된 가상 함수는 상위 클래스의 가상 함수 대신 호출되며, 다형성을 구현할 수 있음

따라서, 가상 테이블은 클래스의 정의 시점에 만들어지며, 객체의 가상 함수 호출은 해당 객체의 가상 함수 포인터 테이블을 통해 이루어짐 상속 관계에서 파생 클래스는 기본 클래스의 가상 함수 테이블을 상속받으며,

재정의한 함수에 대한 포인터가 파생 클래스의 가상 함수 테이블에 저장됨

 

스마트 포인터와의 관계

가상 테이블(virtual table)에는 스마트 포인터 정보가 저장되지 않음

(스마트 포인터는 C++에서 메모리 관리를 자동화하기 위한 도구로 사용되며, 동적으로 할당된 객체를 다루는 포인터)

스마트 포인터는 객체의 수명을 관리하며, 가상 테이블과는 관련은 없음

가상 테이블은 객체의 가상 함수 호출을 위한 메커니즘을 제공하는 데 사용되며, 스마트 포인터는 객체의 수명을 관리하는 데 사용

 

따라서, 가상 테이블에는 가상 함수 포인터만 저장되며, 스마트 포인터 정보는 저장되지 않음

 

스마트 포인터가 사용되는 경우, 가상 함수가 호출되는 시점에 객체가 여전히 유효한지 확인하기 위해 스마트 포인터가 사용될 수 있음

이 경우, 스마트 포인터가 가상 테이블과는 별개로 사용됨

 

 

 

 

 

 

stream 클래스 상속 관계

C++의 입출력 라이브러리는 스트림(Stream) 클래스를 기반으로 구현되어 있는데,

이 스트림 클래스는 데이터를 읽고 쓰는 데 필요한 기본적인 인터페이스를 제공하며,

이를 상속하는 다양한 클래스를 통해 입출력 기능을 확장할 수 있음

 

가장 기본이 되는 스트림 클래스는 istream, ostream, iostream 가 있음

istream은 입력 스트림을, ostream은 출력 스트림을, iostream은 입력 및 출력을 모두 지원하는 스트림 클래스

 

이들 클래스는 각각 ifstream, ofstream, fstream 클래스에서 상속받아 파일 입출력을 지원.

istringstream, ostringstream, stringstream 클래스에서 상속받아 문자열을 입출력하는 기능을 지원.

 

스트림 클래스 상속 구조를 이용하면, 입출력 기능을 다양한 방법으로 확장 가능

예를 들어, istream 클래스에서 상속받아 구현한 새로운 클래스를 통해, 입력 데이터를 다른 형식으로 파싱하거나, 입력 스트림에서 특정 패턴을 찾아 처리할 수 있습니다. 마찬가지로, ostream 클래스에서 상속받아 구현한 새로운 클래스를 통해, 출력 데이터를 다른 형식으로 변환하거나, 출력 스트림에서 특정 패턴을 찾아 처리할 수 있음

 

1) istream 클래스에서 상속받아 구현한 새로운 클래스를 통해, 입력 데이터를 다른 형식으로 파싱하는 예제

 

#include <iostream>
#include <sstream>

class MyInputStream : public std::istream {
public:
    MyInputStream(std::streambuf* buf) : std::istream(buf) {}

    int readInt() {
        int n;
        *this >> n; // 입력 스트림에서 정수를 읽어옴
        std::string s = std::to_string(n); // 정수를 문자열로 변환
        std::reverse(s.begin(), s.end()); // 문자열을 뒤집음
        return std::stoi(s); // 뒤집은 문자열을 다시 정수로 변환하여 반환
    }
};

int main() {
    std::istringstream iss("1234");
    MyInputStream mis(iss.rdbuf()); // MyInputStream 객체 생성

    int n = mis.readInt(); // MyInputStream 클래스에서 추가한 readInt() 함수 사용
    std::cout << "Reversed integer: " << n << std::endl; // 출력 결과: Reversed integer: 4321

    return 0;
}

MyInputStream :  istream 클래스에서 상속받아 정수를 읽어오는 readInt() 함수를 추가한 새로운 클래스

readInt 함수:  입력 스트림에서 정수를 읽어와 문자열로 변환한 후, 문자열을 뒤집어 다시 정수로 변환하여 반환

 

main() 함수에서는 istringstream 객체를 생성하고, 이를 MyInputStream 클래스의 객체로 래핑하여 입력 데이터를 처리.

이 때 MyInputStream 클래스에서 추가한 readInt() 함수를 사용하여 입력 데이터를 다른 형식으로 파싱

 

 

#include <iostream>
#include <sstream>

class MyInputStream : public std::istream {
public:
    MyInputStream(std::streambuf* buf) : std::istream(buf) {}

    MyInputStream& operator>>(int& n) {
        std::string s;
        *this >> s; // 입력 스트림에서 문자열을 읽어옴
        if (s[0] == '-') { // 입력값이 음수인 경우
            std::reverse(s.begin() + 1, s.end()); // 음수 부호를 제외한 문자열을 뒤집음
            n = -std::stoi(s); // 뒤집은 문자열을 음수로 변환하여 반환
        }
        else { // 입력값이 양수인 경우
            std::reverse(s.begin(), s.end()); // 문자열을 뒤집음
            n = std::stoi(s); // 뒤집은 문자열을 정수로 변환하여 반환
        }
        return *this;
    }
};

int main() {
    std::istringstream iss("-1234");
    MyInputStream mis(iss.rdbuf()); // MyInputStream 객체 생성

    int n;
    mis >> n; // MyInputStream 클래스에서 추가한 >> 연산자 사용
    std::cout << "Reversed integer: " << n << std::endl; // 출력 결과: Reversed integer: -4321

    return 0;
}

MyInputStream : istream 클래스에서 상속받아 >> 연산자 오버로딩을 추가한 새로운 클래스

operator>> 함수 : 입력 스트림에서 문자열을 읽어와 음수인 경우 음수 부호를 제외한 문자열을 뒤집고 음수로 변환하거나, 양수인 경우 문자열을 뒤집어 정수로 변환하여 반환

main() 함수에서는 istringstream 객체를 생성하고, 이를 MyInputStream 클래스의 객체로 래핑하여 입력 데이터를 처리

이 때 MyInputStream 클래스에서 추가한 >> 연산자를 사용하여 입력 데이터를 다른 형식으로 파싱

 

 

 

2) 입력 스트림에서 특정 패턴을 찾아 처리

입력 스트림에서 특정 패턴을 찾아 처리하는 예제 코드

이 예제에서는 입력 스트림에서 "[[name]] : [age]" 형태의 패턴을 찾아, name과 age를 출력

 

 

#include <iostream>
#include <string>
#include <regex>

int main() {
    std::string input = "John : 30\n[[Sarah]] : 25\n[[David]] : 40\nMary : 35";

    std::regex pattern(R"(\[\[(\w+)\]\] : (\d+))");
    std::smatch match;

    std::string::const_iterator search_start(input.cbegin());
    while (std::regex_search(search_start, input.cend(), match, pattern)) {
        std::cout << "Name: " << match[1].str() << ", Age: " << match[2].str() << std::endl;
        search_start = match.suffix().first; // 처리한 패턴 뒷부분으로 검색 시작 위치를 갱신함
    }

    return 0;
}

 

 

std::regex 클래스를 사용하여 "[[name]] : [age]" 형태의 패턴을 정규 표현식으로 표현

이후 std::regex_search 함수를 사용하여 입력 문자열에서 패턴을 찾아 match 객체에 저장

while 루프에서는 검색 시작 위치를 갱신하면서 계속해서 패턴을 찾아 처리

std::smatch 객체의 [] 연산자를 사용하여 패턴에서 추출한 name과 age 값을 출력

입력 문자열에서 "[[name]] : [age]" 형태의 패턴이 여러 번 나오기 때문에, while 루프를 사용하여 모든 패턴을 찾아 처리

이때 search_start 변수를 사용하여 처리한 패턴 뒷부분으로 검색 시작 위치를 갱신

 

 

 

입력 스트림에서 특정 패턴을 찾아 처리하는 좀 더 복잡한 예제 코드

이 예제에서는 입력 스트림에서 "Name: [name]\nAge: [age]\n" 형태의 패턴을 찾아, name과 age를 출력

이때 입력 스트림으로부터 데이터를 읽는 과정에서 에러가 발생하는 경우, 해당 에러 메시지를 출력하고 다음 데이터를 처리

 

 

#include <iostream>
#include <sstream>
#include <string>
#include <regex>

int main() {
    std::string input = "Name: John\nAge: 30\nName: Sarah\nAge: 25\nName: David\nAge: 40\nName: Mary\nAge: invalid\nName: Tom\n";

    std::istringstream stream(input);
    std::regex pattern(R"(Name: (\w+)\nAge: (\d+))");
    std::smatch match;

    std::string name, age;
    std::string line;
    while (std::getline(stream, line)) {
        if (std::regex_match(line, match, pattern)) {
            name = match[1].str();
            age = match[2].str();
            std::cout << "Name: " << name << ", Age: " << age << std::endl;
        } else if (line.find("Name: ") == 0) {
            std::cerr << "Invalid input: " << line << std::endl;
        }
    }

    return 0;
}

 

 

 

입력 문자열에서 "Name: [name]\nAge: [age]\n" 형태의 패턴을 std::regex 클래스를 사용하여 정규 표현식으로 표현

이후 std::getline 함수를 사용하여 입력 스트림에서 데이터를 한 줄씩 읽어와 처리

while 루프에서는 std::getline 함수를 사용하여 입력 스트림에서 한 줄씩 데이터를 읽어옴

이때 std::regex_match 함수를 사용하여 읽어온 데이터가 패턴과 일치하는지 확인

일치하는 경우에는 match 객체에 저장된 name과 age 값을 출력

패턴과 일치하지 않는 데이터가 입력 스트림에서 읽혀올 경우에는 에러 메시지를 출력

이때, 입력 스트림으로부터 다음 데이터를 처리하기 위해 std::cerr 객체를 사용하여 에러 메시지를 출력

위 예제 코드에서는 입력 스트림으로부터 데이터를 읽는 과정에서 에러가 발생하는 경우, 해당 에러 메시지를 출력하고 다음 데이터를 처리 이를 통해 입력 데이터의 오류를 검출하고 처리할 수 있음

 

ostream 클래스에서 상속받아 구현한 새로운 클래스를 통해, 출력 데이터를 다른 형식으로 변환하는 예제

 

ostream 클래스에서 상속받아 구현한 새로운 클래스를 통해, 출력 데이터를 다른 형식으로 변환하는 예제입니다. 이 예제는 std::ostream을 상속받아 std::ostream으로 출력되는 데이터를 16진수로 변환하여 출력하는 HexOstream 클래스를 구현한 예제

 

 

#include <iostream>
#include <iomanip>

class HexOstream : public std::ostream
{
public:
    HexOstream(std::ostream& os) : std::ostream(os.rdbuf()) {}

    HexOstream& operator<<(int value)
    {
        std::ostream& os = *this;
        os << std::hex << std::setw(2) << std::setfill('0') << value;
        return *this;
    }
};

int main()
{
    HexOstream hex_os(std::cout);

    int value = 123;
    hex_os << "value in hex: ";
    hex_os << value;

    return 0;
}

 

 

위 예제에서 HexOstream 클래스는 std::ostream 클래스를 상속받아 구현되

HexOstream 클래스는 operator<< 연산자를 오버로딩하여 16진수로 변환하여 출력

HexOstream 클래스의 operator<< 연산자는 인자로 받은 int 타입의 value 변수를 16진수로 변환하여 출력

이를 위해 std::hex 조작자를 이용하여 16진수 출력 모드로 변경하고, std::setw와 std::setfill 조작자를 이용하여 출력 폭을 맞추고 빈 자리는 0으로 채움

main() 함수에서는 HexOstream 클래스를 생성하여 std::cout 스트림을 인자로 전달

그리고 int 타입의 value 변수를 생성하고, HexOstream 클래스를 이용하여 value 변수를 출력

 

 

출력 결과

value in hex: 7b

 

 

std::ostream을 상속받아 사용자 정의 출력 스트림 클래스 MyOstream을 구현한 예제

MyOstream 클래스는 기존 출력 스트림에 대해 스트림 버퍼를 이용해 대문자로 변환하여 출력하는 기능을 추가

 

 

#include <iostream>
#include <streambuf>
#include <locale>
#include <algorithm>

class MyOstream : public std::ostream {
public:
    MyOstream(std::ostream& stream)
        : std::ostream(stream.rdbuf()), _streambuf(stream.rdbuf())
    {}

    MyOstream& operator<< (const char* str) {
        std::transform(str, str + strlen(str), str, [](char c) {
            return std::toupper(c, std::locale());
        });

        _streambuf->sputn(str, strlen(str));
        return *this;
    }

private:
    std::streambuf* _streambuf;
};

int main() {
    MyOstream myout(std::cout);
    myout << "Hello, world!" << std::endl;
    return 0;
}

MyOstream 클래스는 std::ostream을 상속받아 구현되

생성자에서 std::ostream으로부터 스트림 버퍼를 가져오며, operator<< 연산자를 오버로딩하여 출력할 문자열을 대문자로 변환하여 출력

operator<< 연산자에서는 입력으로 받은 문자열을 std::transform 알고리즘을 이용하여 대문자로 변환

그리고 스트림 버퍼에 변환된 문자열을 써넣어 출력

main() 함수에서는 MyOstream 클래스를 이용하여 std::cout으로 출력하는 myout 객체를 생성하고, myout 객체를 이용하여 "Hello, world!" 문자열을 출력합니다.

 

 

출력 결과

HELLO, WORLD!

 

 

이와 같이 std::ostream 클래스를 상속받아 사용자 정의 출력 스트림 클래스를 구현하면,

기존의 출력 스트림에 기능을 추가하거나 사용자 정의 출력 형식을 지원할 수 있음

 

 

ostream 클래스에서 상속받아 구현한 새로운 클래스를 통해, 출력 스트림에서 특정 패턴을 찾아 처리하는 예제

 

std::ostream을 상속받아 사용자 정의 출력 스트림 클래스 MyOstream을 구현한 예제

MyOstream 클래스는 기존 출력 스트림에 대해 스트림 버퍼를 이용해 특정 패턴을 찾아서 대체 문자열로 변환하는 기능을 추가

 

 

#include <iostream>
#include <streambuf>
#include <cstring>

class MyOstream : public std::ostream {
public:
    MyOstream(std::ostream& stream, const std::string& pattern, const std::string& replace)
        : std::ostream(stream.rdbuf()), _streambuf(stream.rdbuf()), _pattern(pattern), _replace(replace)
    {}

    MyOstream& operator<< (const char* str) {
        std::string s(str);
        size_t pos = s.find(_pattern);
        while (pos != std::string::npos) {
            s.replace(pos, _pattern.length(), _replace);
            pos = s.find(_pattern, pos + _replace.length());
        }

        _streambuf->sputn(s.c_str(), s.length());
        return *this;
    }

private:
    std::streambuf* _streambuf;
    std::string _pattern;
    std::string _replace;
};

int main() {
    MyOstream myout(std::cout, "world", "WORLD");
    myout << "Hello, world!" << std::endl;
    return 0;
}

 

 

MyOstream 클래스는 std::ostream을 상속받아 구현되었습니다. 생성자에서는 스트림 버퍼와 대체할 패턴과 문자열을 지정 operator<< 연산자를 오버로딩하여 출력할 문자열에서 패턴을 찾아서 대체 문자열로 변환

operator<< 연산자에서는 입력으로 받은 문자열에서 std::string::find 함수를 이용하여 패턴을 찾음

패턴이 있으면 std::string::replace 함수를 이용하여 대체 문자열로 변환

이후 다시 std::string::find 함수를 호출하여 패턴을 찾습니다. 이 과정을 반복하여 패턴이 더 이상 없을 때까지 대체

마지막으로 변환된 문자열을 스트림 버퍼에 써넣어 출력

main() 함수에서는 MyOstream 클래스를 이용하여 std::cout으로 출력하는 myout 객체를 생성하고,

myout 객체를 이용하여 "Hello, world!" 문자열을 출력

 

 

출력 결과

HELLO, WORLD!

 

 

Part1. 일반 사실을 바탕으로 하는 일반 조건절

걔가 아무말도 안하면, 걔가 화났다는 거야

 

☞ If he doesn't say anything, that means he is upset.

 

냄새가 안좋으면, 아마 먹기에 안전하지 않다는거야

 

☞ If it smells bad, it's probably not safe to eat

 

거기 사람이 너무 많으면, 다른데 가는게 나을거 같아

 

☞ If there are too many people, it's better to go somewhere else

 

지하철 타면, 더 빨라

 

☞ If you take the subway, it's faster

 

5시 전에 나가면, 차 별로 안막힐거야

 

☞ If you leave before 5 o'clock, there isn't much traffic


Part2. 현재 사실을 뒤집어서 조건절로

< if + 과거 시제 , could / would be able to >

내 학교에서 먼 곳에 살아, 그래서 아침에 학교가는데 시간이 오래 걸려

학교에 좀 더 가까이 살았으면, 시간을 좀 더 절약할 수 있을 텐데

 

I live far from school, so it takes a lot of time to go to school in the morning

☞ If I lived closer to school, I could(would be able to) save more time.

       live 안됨                                       can  안됨

 

나 일주일에 6번 일하러가. 그래서 주말에 계획 세우는게 어려워

일주일에 딱 5일만 간다면, 주말에 더 많은 것들을 할 수 있을텐데

 

I go to work 6 times a week. So it's hard to make plans on weekends

If I went to work just 5 days a week, I would be able to(could) do more things on weekends.

 

난 외장하드를 하나 갖고 있어, 그래서 어딜가든 음악이나 영화 갖고 다닐 수 있어

이게 없다면, 그러진 못할 거야

 

I have an external hard drive, so i can carry my music and movies wherever I go

☞ If I didn't have one, I woudn't be able to (couldn't) do that

 

*one : 일반적인 외장하드 (꼭 내 드라이브가 아니더라도)

 

나는 살이 쉽게 쪄, 그래서 먹는걸 조심해야돼

내가 쉽게 찌지 않는다면, 좀더 자유롭게 음식을 즐길텐데

 

I gain weight quite easily, so I have to be careful about what i eat.

☞ But if I didn't gain weight so easily, I would be able to enjoy food more freely

 

저는 경험이 많지 않아서요 , 그래서 시간이 오래 걸려요

경험이 많았으면, 그렇게 오래걸리진 않았을거에요

 

I don't have any experience, so it takes a lot of time

☞ If I had a lot of experience, it wouldn't take so long


Part3. 조건절 미래 (조건절은 현재형 시제)

1) 어떤 조건 하에 나의 의지/계획/인과관계 (be going to)

   

   이번주에 비오면, 집에 있을려고

 

   If it rains this weekend, I'm going to stay home.

 

   그녀가 물어보면, 나 그냥 아무것도 모르는척 할거야

 

  ☞ If she asks, I'm just gong to pretend I don't know anything

 

   오늘 일찍 퇴근하면, 친구들 몇명 만나서 술한잔 하게

 

   ☞ If I get off work early today, I'm going to meet up with some friends for some drinks.

 

   이런 일이 또 발생하면 걔들은 모든걸 잃을걸

 

   ☞ If this happens again, they're going to lose everything


2) 어떤 조건 하에 ~ 을 할 수도 있다/할지도 모른다/ 해도 된다/ 하면 된다 (can / might/ don't have to )

    이게 마음에 안들면, 환불 해도돼  (환불 :  refund 보다는  return / send it back 을 좀 더 많이 씀 )

 

   ☞ If you don't like it, you can return it / send it back

 

    진짜 열심히 공부하면, 시험 통과할 수도 있어        

 

    If you study really hard, you might pass the test

 

    늦으면 택시 타도돼

 

    If you're late, you can just take a taxi

 

    그녀한테 사실대로 얘기하면, 이해해줄수도 있어

 

    If you tell her the truth, she might understand

 

   배 안고프면 안먹어도돼

 

    You don't have to eat if you're not hungry


3) 조동사+조동사  or 시제 + 조동사  (might have to / might be able to  or  will be able to / be going to be able to  )

   "~ 해야 될 수도 있어" /   "~ 할 수도 있어" or   "~해야 할거야" (미래 + 가능성)

 

   내가 돈을 충분히 못 모으면 빌려야 될 수도 있어

 

  ☞ If I can't save enough money, I might have to borrow some. (아주 많이 씀 ; ~해야 될수도있다) 

 

   오늘 회의에서 마이클이 발표 하기로 돼있어. 근데 걔가 늦으면 너가 해야될 수도 있어

 

  ☞ Michael is supposed to give a presentation in the meeting today. But if he's late, you might have to do it

 

   걔들이 우릴 도와주지 않으면, 우리는 이걸 제시간에 끝낼 수 없을 거야

 

  ☞ If they don't help us, we won't be able to finish this on time.

 

   우리 지금 가면 막차 탈 수도 있어

 

   ☞ If we leave(go) now, we might be able to catch the last train/bus

 

   날씨가 진짜 좋으면, 좋은 사진들 좀 찍을 수 있을 거야 

 

   ☞ If the weather is really clear(nice), we're going to be able to  takes some nice picture


Part4. 과거에 이미 일어났던일을 반대로 가정

 

 1) ~ 을 했을거야 / 이랬을거야 / 저랬을거야 (would have pp)

 

저 어제 늦게 잤어요. 그래서 오늘 아침에 일어나는게 힘들었어요

어제 좀 더 일찍 잤다면, 오늘 일어나는게 그렇게 힘들지 않았을거야

 

☞ I went to be late yesterday and/ so it was hard to get up this morning

    If I had gone to bed earlier last night / yesterday, It wouldn't have been so hard to get up this morning

 

내가 준비가 안됐었어, 그래서 인터뷰 잘 못했어

좀 더 준비가 돼있었으면, 좀 더 잘 할 수 있었을 텐데 

 

☞ I wasn't prepared, so I didn't do well on the interview

     If I had been more prepared, I would've done better (on the interview)

 

아침을 안먹었어서, 시험보는 동안 너무 배고팠었어

내가 아침 먹었었더라면, 시험 보는 동안 그렇게 배고프진 않았을거야

 

I didn't eat breakfast, so I was very hungry during the test

    If I had eaten breakfast, I wouldn't have been so hungry during the test and I would've concentrated batter'

 

걔 상황을 몰랐어, 그래서 못도와 줬어

걔 상황을 알았었다면 도와 줬을 텐데

 

☞ I didn't know about his situation, so I couldn't help him

     If I had known about his situation, I would've helped him


2) ~을 할 수도 있었을 거야 / ~ 했었을 지도 몰라 (might have pp)

 

걔가 나한테 물어봤었다면, 도와줬을 지도 몰라

 

☞ If he had asked me, I might've helped him

 

그녀가 좀 더 열심히 연습을 했었으면, 그 실수를 안했을 지도 몰라

 

☞ If she had practiced harder, she might not've made that mistake

 

내가 좀 더 알아봤더라면, 다른 결정을 했었을지도 몰라

 

☞ If I had looked into it (a little more), I might've made a different decision/ chosen a different one

 

*look into something : ~을/에 대해서 알아보다

 

그가 그녀한테 말 안했었으면  걔는 그게 얼마나 중요한건지 몰랐을지도 몰라

 

☞ If he hadn't told her, she might not've realized how important it was


3) ~을 할 수도 있었을 거야 / ~ 할 수 있었을 텐데 (could have pp)

 

공부를 좀 열심히 했었더라면,더 좋은 학교에 갈 수 있었을텐데

 

 If I had studied harder, I could've gone to a better school

                                       (would have been able to)

*could've : 더 좋았었을 결과에 대해 말할 때 (아쉬움이 있는 뉘앙스)

*might've : 추측

 

나한테 물어봤었으면, 내가 도와줄수 있었는데

 

☞ If you had asked me, I could've helped you

 

니가 도와주지 않았다면, 제시간에 못끝냈을 거야

 

If you had not helped me, I couldn't have finished this on time

 

우리가 조금이라도 늦게 떠났다면, 여기에 이시간에 도착못햇을거야

 

 If we had left a little later, we couldn't have arrived here at this time


4) ~해야 했을거야 / 해야 됐었을거야 / (부정형) ~하지 않아도 됐을거야 (would've had to, would've needed to)

 

내가 돈을 충분히 모으지 못했었으면, 거기에 1년 더 있었어야 했을거야

 

☞ If I hadn't saved enough money, I would've had to be(stay) there for another year

 

그녀가 시험을 합격 못했었으면, 1년더 (시험을) 준비해야 했을 거야

 

☞ If she hadn't passed the exam, she would've had to prepare another year for the test // preapre for the test for another year

 

너가 좀 더 일찍 얘기해줬으면, 내가 이렇게 오래 안기다려도 됐었잖아

☞ If you had told me a little earlier, I wouldn't have had to wait this long

 

내가 그 실수를 하지 않았으면, 우리가 이걸 다시 할 필요가 없었을거야

☞ If I hadn't made that mistake, we wouldn't have had to do this again

 


Practice

 

1. If I knew I got to regret not to speak English well, I would've studied harder and harder

 

☞ 영어에서는 '후회할줄 알았으면' 이라는 표현을 쓰지 않음

☞ If I knew(had known) I would need to use English, I would've studied harder.

 

2. If I knew your phone number, I would call you

 

☞ 과거로 바꾸면,

If I knew(had known) your phone number, I would've called you

 

3. If I knew LA earlier, I wouldn't have difficulty in learning English.

 

knew + something 인 경우, 전치사 about 필요 (사람인 경우에는 필요없음)

☞ If I knew(had known) about LA earlier, I wouldn't have had such a difficult time learning english.

 

4. If I had known it had car accident, I would have fastened seat belt.

 

☞ If I had known I would be in a car accident, I would've fastened seat belt

☞ If I had known there would be a car accident, I would've fastened my seat belt

 

5. If I had known about that accident, I wouldn't have gone through a rough period. I should've known it.

 

 이미 일어난 일에 대한 시간에 대해 표현 "such" a rough period

☞  If I had known that happened (about that), I wouldn't have gone through such a rough period.

I should've know about it

 

6. If I had had money enough, I would have bought the house

 

☞  had + had  는 가능하면 피하는 것이 좋음(?) 

☞  If I had enough money, I would have bought the house

 

7. If I knew about the Live Academy earlier, I wouldn't have spent my time exploring other Youtube channels

 

이미 일어난 일에 대한 시간 "so much time"

If I knew about the Live Academy earlier, I wouldn't have spent so much time exploring other Youtube channels

 

8. If I had known she'd been already married, I wouldn't have tried to asking her phone number 

 

☞ 이미 결혼한 상태 (was already married), 물어보는 것은 선택 (굳이 노력 언급 X)

전화번호를 달라고 하다: asked 'for' her phone number

If I had known she was already married, I wouldn't have asked for her phone number

 

9. If I knew that was wrong, I wouldn't have done that

 

10. If I had known the fact that she already dated another guy, I wouldn't have loved her

 

☞ 만나고 있는 중 이므로  was dating, love 보다는 관심을 갖다 interested in

If I had known she was (already) dating another guy, I wouldn't have taken interest in her

 

11. If I had known what I know when I was in college, I would have live a better life.

 

☞ Back (when I was) in collge, if I had known what I know now, I would have lived a better life. 13'

+ Recent posts