상수 객체 (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는 상수 포인터가 되어, 가리키는 값을 변경할 수 없으므로, 값이 변경되지 않도록 보장할 수 있습니다

 

 

 

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를 지정하여 메모리 할당과 해제를 처리할 수 있습니다.

 

 

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!

 

헤더 파일은 별도 파일에 구현되어있는 함수와 클래스의 인터페이스를 선언 및 제공하기 위한 용도로 사용됨

 

즉, 헤더파일은 보통 실제 구현 코드는 포함하지 않고, 프로그램의 API와 같은 형태로 제공되는 용도로 사용됨

 

Build process 에서 C++ 컴파일러는 헤더파일을 읽어 프로그램 상에서 사용 되는 함수와 클래스를 파악함

별도 소스파일에 존재하는 실제 구현 코드는 object 파일로 컴파일 됨

결국 object 파일은 링킹(linking) 과정을 통해 최종 실행 파일 혹은 라이브러리를 만들어 내게 되는 것

 

헤더파일들은 여러가 지 소스파일들에서 코드 중복을 방법을 제공하기도 함

예를들면, 한 프로그램이 다른 소스 파일에 정의된 유틸리티 함수를 사용한다면, 해당 함수는 헤더파일에 선언될 수 있으며, 다른 소스파일에서는 그것을 include 함으로써 사용할 수 있게 됨

이렇게 함으로서 코드가 중복이 없는 정제된 코드를 만들어 낼 수 있음

 

헤더 파일은 build process 에서 프로그램의 요소(component)들의 인터페이스와 구현을 분리하는 방법이며, 코드 중복을 줄일 수 있는 아주 중요한 역할을 함

 

 

 

 

Header files provide a number of benefits for building C++ programs:

  1. Code organization: Header files provide a way to separate the interface and implementation of a program's components, making it easier to organize and maintain the code.
  2. Code reuse: Header files make it easy to reuse code across multiple source files, as functions and classes can be declared in a header file and included in any source file that needs to use them.
  3. Dependency management: Header files make it clear which entities are used by each source file, making it easier to manage dependencies between different components of the program.

virtual function (가상함수) 오버라이딩 두가지 규칙

1. 상속된 메소드를 재정의할 경우에는 오리지널 원형과 정확히 일치시킬 필요가 있음

(에외: 리턴형의 공변(covariance)- 리턴형이 기초 클래스에 대한 참조나 포인터인 경우 파생 클래스에 대한 참조나 포인터로 대체)

2. 기초 클래스 선언이 오버로딩 되어있다면, 파생 클래스에서 모든 기초 클래스 버전들을 재정의 해야 

 

Abstract class (추상 클래스) 의 특징

적어도 하나의 순수(pure) 가상함수를 가진 클래스

순수가상함수(pure virtual function) : 함수 선언 뒤에 “=0” 붙여서 표현하며 선언만으로 존재

순수가상함수를 포함한 클래스는 객체생성 불가능(기초역할을 하기위해서만 존재)

 

상속과 오버라이딩(Inheritance and Overriding)

기초 클래스 메소드를 파생 클래스에서 재정의 할때는 일반적으로 해당 기초 클래스 메소드를 virtual 로 선언함

virtual 키워드 사용하지 않을 경우, 프로그램은 참조형이나 포인터형에 기초하여 메소드를 선택

virtual 키워드 사용할 경우, 프로그램은 참조나 포인터에 의해 지시되는 객체형에 기초하여 메소드 선택

 

※ 주의: 파생 클래스에 하나라도 오버라이딩 된 함수가 존재할 경우, 기초 클래스에 가상 소멸자 선언해줘야 함

virtual 소멸자가 아니라면, 포인터형에 해당하는 소멸자만 호출될 것

(즉, 파생 클래스를 지시하는 경우에도 기초 클래스 소멸자가 호출된다는 것을 의미)

따라서 virtual 소멸자로 객체형에 해당하는 소멸자가 호출된 이후 기초 클래스 소멸자가가 자동으로 호출되게함

 

정적 결합과 동적 결합(Static binding & Dynamic binding)

 

정적 결합: 컴파일 동안 일어나는 결합 (초기 결합)

동적 결합: 런타임 동안 올바른 가상 메소드가 선택되도록 하는 결합(말기 결합)

 

객체에 대한 참조를 사용하여 또는 객체를 지시하는 포인터를 사용하여 가상 메소드가 호출되면,

프로그램은 그 참조나 포인터형을 위해 정의된 메소드를 사용하지 않고, 

객체형을 위해 정의된 메소드를 사용하며 이것을 “동적 결합”이라고 함

 

기초 클래스 포인터나 참조가 파생 클래스 객체를 지시하는 것이 항상 가능하므로 이러한 로직이 중요함

(암시적) 업캐스팅: 기초 클래스 포인터나 참조가 기초 클래스 객체나 파생 클래스 객체를 참조하는 것을 가능하게 함

 

동적결합이 필요한 이유

이에 대한 해답으로 가상 멤버 함수 개념이 등장

 

friend (프렌드) 와 virtual function 가상함수)

프렌드는 가상함수가 될 수 없음

멤버 함수만 가상함수가 될 수 있는데, 프렌드는 클래스 멤버가 아니기 때문

이 경우 프렌드 함수 내부적으로 가상 멤버 함수를 사용하게 하면됨

 

가상함수를 다시 정의하지 않으면, 파생 클래스는 그 함수의 기초 클래스 버전을 사용함

(길게 이어진 파생 사슬 구조라면 가장 최근에 정의된 버전을 사용함)

 

어떤 함수를 파생 클래스에서 재정의하면 동일한 함수 시그니처를 갖고 있는 기초 클래스 선언만 가리는 것이 아니라,

매개변수 시그니처와 상관 없이 같은 이름을 가진 모든 기초 클래스 메소드를 가림

 

 

가상함수 테이블의 생성 및 참조

virtual table 는 컴파일러에 의해 생성됨

생성하는 조건은 클래스 내부에 최소 하나의 가상함수가 정의 되어 있는 경우임

가상함수 테이블은 해당 클래스의 가상함수 리스트를 포함하고 있는 자료 구조를 의미함

 

가상함수를 포함하고 있는 클래스의 각 객체들은 가상함수 테이블을 가리키는 포인터를 가지는데,

이 포인터는 객체의 생성자에 의해 초기화 되며, 해당 클래스의 가상함수 테이블을 가리킴

 

객체에 의해 가상함수가 호출되는 것은, 가상함수 테이블 포인터를 이용하여 가상함수 테이블 내에 호출된 함수를 찾아 호출하게 되는 것

 

가상함수 테이블은 객체 포인터나 참조에서 가상함수가 호출되는 런타임 때에 참조 됨

컴파일러는 가상함수 테이블 포인터를 사용하는 코드를 생성함으로써, (객체의 실제 타입을 근거로 )정확한 함수를 찾아내게 됨

이러한 과정을 dynamic dispatch 라고 하며, 이 과정을 통해 런타임에 객체의 실제 타입으로부터 정확한 함수가 호출될 수 있게 되는 것임

 

가상함수 및 테이블의 사용은 C++의 핵심 기술중 하나인 다형성(Polymorphism)을 지원함으로써 유연하고 확장성 있는 코드를 구성할 수 있음

 

결론: virtual table 자체는 컴파일 타임(compile time)동안에 생성되나 조회는 런타임(runtime)에 이루어짐

클래스간 결합도

friend > inheritance > composition ( > aggregation ) > dependency

 

 

용어정리 : 상속?  객체?  캡슐화?

  1. 상속은 기능의 확장이 아니라, 클래스를 분류하는 수단이다
  2. 객체는 책임이 있는 어떤 것이다
  3. 캡슐화는 데이터, 인터페이스, 클래스, 시스템 등 구현 가능한 모든 것에 대한 은닉이다. (캡슐화는 가변성에 대한 은닉이다)

 

상속이 내포하고 있는 문제

상속이란?

기존에 정의되어 있는 클래스의 필드와 메소드를 물려받아 새로운 클래스를 생성하는 기법

중복 코드 제거와 기능 확장을 쉽게 할 수 있음

클래스들 간 계층적인 구조를 만들 수 있음

 

하위 클래스가 상위 클래스의 구현에 의존하므로 (상위 클래스)변경에 취약

 

- 상속은 부모 클래스와 강하게 의존하고, 부모 클래스의 캡슐화를 해치고, 결합도가 높아짐

- 부모 클래스 구현을 변경하면, 많은 자식 클래스를 모두 변경해줘야 하는 상황이 생길 수 있음

 

상위 클래스의 모든 퍼블릭 메소드가 하위 클래스에도 반드시 노출됨(불필요한 메소드 상속받는 문제)

 

- 상속은 불필요한 부모 클래스의 퍼블릭 메소드가 자식 클래스도 어쩔 수 없이 노출하게 됨

- 특히 공개된 부모 클래스의 퍼블릭 메소드가 자식 클래스의 내부 규칙과 맞지 않을 수 있음

 

조합은 이 문제를 어떻게 해결할 수 있나?

 

- 상속과 달리 부분 객체의 내부 구현이 공개되지 않음

  - 메소드를 호출하는 방식으로 퍼블릭 인터페이스에 의존해서 부분 객체의 내부 구현이 변경되어도 비교적 안전

- 부분 객체의 모든 퍼블릭 메소드를 공개하지 않아도 됨

 

조합하고 싶은 클래스의 인스턴스를 새로운 클래스의 private필드로 참조, 

그 다음 인스턴스의 메소드를 호출하는 방식으로 구현

 

조합하면 부분 객체의 캡슐화를 지킬 수 있음

조합은 부분 객체의 퍼블릭 인터페이스에 의존함

 

조합은 부분객체의 모든 공개 메소드를 노출하지 않아도 됨

 

상속과 조합을 어떻게 사용해야 할까?

 

상속의 목적

서브 타이핑 (다형적 계층구조 구현) - 부모 자식 행동이 호환

서브 클래싱 (다른 클래스의 코드 재사용) - 부모 자식 행동이 호환 x

 

상속을 고려할때

1. 두 객체가 서로 is-a  관계인가 (조합은 has-a 관계)

2. 클라이언트 관점에서 두 객체가 동일한 행동을 할 것이라 기대하는가

 

결론: 단순히 코드 재사용하려면 조합, 동일하게 행동하는 인스턴스를 그룹화 하려면 상속.

 

 

 

1. 서로 다른 클래스 상에서는 같은 이름으로 enum type이 정의되더라도 독립적으로 사용 가능함

 

class A
{
    public:

    enum Color
    {
        red = 1,
        blue = 2,
        green = 3
    };
    
    void funcA()
    {
        cout<< Color::red <<" "<< Color::blue <<" "<<Color::green <<endl;
    }
};

class B
{
    public:

    enum Color
    {
        red = 11,
        blue = 22,
        green = 33
    };
    
    void funcB()
    {
        cout<< Color::red <<" "<< Color::blue <<" "<<Color::green <<endl;
    }
};

int main()
{
    A a;
    B b;
    
    a.funcA(); // 1,2,3
    b.funcB(); // 11,22,33
    

    return 0;
}

 

2. 그러나 같은 클래스 내에서는 enum 타입이 다르더라도, 동일한 이름의 enum 변수가 존재할 경우 compile error

 

class A
{
    public:

    enum Color
    {
        red = 1,
        blue = 2,
        green = 3
    };

    enum AdvancdedColor
    {
        red = 100,
        blue = 200,
        green = 300
    };
    
    void funcA()
    {
        cout<< Color::red <<" "<< Color::blue <<" "<<Color::green <<endl;
    }
};

int main()
{
    A a;
    a.funcA();  // compile error
    
    return 0;
}

 

컴파일러에서는 enum을 다음과 같이 처리함.

즉, (default type인) uint 형으로 casting 하게 되는데, 이름이 중복되기 때문에 컴파일 에러를 발생시키는 것임.

 

class A
{
  
  public: 
  enum Color
  {
    red = static_cast<unsigned int>(1), 
    blue = static_cast<unsigned int>(2), 
    green = static_cast<unsigned int>(3)
  };
  
  enum AdvancdedColor
  {
    red = static_cast<unsigned int>(100), 
    blue = static_cast<unsigned int>(200), 
    green = static_cast<unsigned int>(300)
  };
  
};

 

3. enum class 는 2번과 같은 상황에서 enum 타입이 다르다면 동일한 변수를 사용할 수 있게끔 해줌

    (enum class 내에 있는 값들은 서로 독립된 값으로 switch 문이나 if 에서 구분지을 때는 바로 사용 가능하나,
     값을 print 하거나 비교하기위해서는 casting 해줘야 함; 아래 예시의 경우에는 int 형으로 type deduction 됨)

 

class A
{
    public:

    enum class Color
    {
        red = 1,
        blue = 2,
        green = 3
    };

    enum class AdvancdedColor
    {
        red = 100,
        blue = 200,
        green = 300
    };
    
    void funcA()
    {
        cout<< static_cast<int>(Color::red) <<" "<< static_cast<int>(AdvancdedColor::red) <<endl;
    }
};

int main()
{
    A a;
    a.funcA();
    

    return 0;
}

 

컴파일러가 enum class 를 인식한 결과는 아래와 같음

 


  enum class Color : int
  {
    red = 1, 
    blue = 2, 
    green = 3
  };
  
  enum class AdvancdedColor : int
  {
    ared = 100, 
    ablue = 200, 
    agreen = 300
  };

 

+ Recent posts