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.

클래스간 결합도

friend > inheritance > composition ( > aggregation ) > dependency

 

 

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

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

 

상속이 내포하고 있는 문제

상속이란?

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

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

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

 

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

 

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

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

 

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

 

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

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

 

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

 

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

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

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

 

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

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

 

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

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

 

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

 

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

 

상속의 목적

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

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

 

상속을 고려할때

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

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

 

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

 

 

 

 

 

#include <iostream>

template<typename PreviousDispatcher, typename Msg, typename Func>
class TemplateDispatcher
{
  
  public:
  TemplateDispatcher(){}
  template<typename OtherMsg, typename OtherFunc>
  TemplateDispatcher<TemplateDispatcher,OtherMsg,OtherFunc>handle(OtherFunc&& f){
      return TemplateDispatcher<TemplateDispatcher,OtherMsg,OtherFunc>();
  }
      
};

class dispatcher
{
  public:
  template<typename Message, typename Func>
  TemplateDispatcher<dispatcher,Message,Func> handle(Func&& f){
    return TemplateDispatcher<dispatcher,Message,Func>();
  }
};


class receiver
{
    public:
    dispatcher wait()
    {
        return dispatcher();
    }
};


int main(void)
{
        receiver r;
        r.wait()
        .handle<int>([&]{})
        .handle<double>([&]{});
  
        return 0;
}

 

 

 

#include <iostream>

template<typename PreviousDispatcher, typename Msg, typename Func>
class TemplateDispatcher
{
  
  public:
  inline TemplateDispatcher()
  {
  }
  
  template<typename OtherMsg, typename OtherFunc>
  inline TemplateDispatcher<TemplateDispatcher<PreviousDispatcher, Msg, Func>, OtherMsg, OtherFunc> handle(OtherFunc && f)
  {
    return TemplateDispatcher<TemplateDispatcher<PreviousDispatcher, Msg, Func>, OtherMsg, OtherFunc>();
  }
};

/* First instantiated from: insights.cpp:40 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
class TemplateDispatcher<dispatcher, int, __lambda_40_22>
{
  
  public:
  inline TemplateDispatcher()
  {
  }
  
  template<typename OtherMsg, typename OtherFunc>
  inline TemplateDispatcher<TemplateDispatcher<dispatcher, int, (lambda)>, OtherMsg, OtherFunc> handle(OtherFunc && f);
  
  /* First instantiated from: insights.cpp:41 */
  #ifdef INSIGHTS_USE_TEMPLATE
  template<>
  inline TemplateDispatcher<TemplateDispatcher<dispatcher, int, __lambda_40_22>, double, __lambda_41_25> handle<double, __lambda_41_25>(__lambda_41_25 && f)
  {
    return TemplateDispatcher<TemplateDispatcher<dispatcher, int, __lambda_40_22>, double, __lambda_41_25>();
  }
  #endif
  
};

#endif
/* First instantiated from: insights.cpp:41 */
#ifdef INSIGHTS_USE_TEMPLATE
template<>
class TemplateDispatcher<TemplateDispatcher<dispatcher, int, __lambda_40_22>, double, __lambda_41_25>
{
  
  public:
  inline TemplateDispatcher()
  {
  }
  
  template<typename OtherMsg, typename OtherFunc>
  inline TemplateDispatcher<TemplateDispatcher<TemplateDispatcher<dispatcher, int, (lambda)>, double, (lambda)>, OtherMsg, OtherFunc> handle(OtherFunc && f);
};

#endif


class dispatcher
{
  
  public:
  template<typename Message, typename Func>
  inline TemplateDispatcher<dispatcher, Message, Func> handle(Func && f)
  {
    return TemplateDispatcher<dispatcher, Message, Func>();
  }
  
  /* First instantiated from: insights.cpp:40 */
  #ifdef INSIGHTS_USE_TEMPLATE
  template<>
  inline TemplateDispatcher<dispatcher, int, __lambda_40_22> handle<int, __lambda_40_22>(__lambda_40_22 && f)
  {
    return TemplateDispatcher<dispatcher, int, __lambda_40_22>();
  }
  #endif
  
};




class receiver
{
  
  public:
  inline dispatcher wait()
  {
    return dispatcher();
  }
  
  // inline constexpr receiver() noexcept = default;
};




int main()
{
  receiver r = receiver();
      
  class __lambda_40_22
  {
    public:
    inline /*constexpr */ void operator()() const
    {
    }
    
    
  };
  
  
  class __lambda_41_25
  {
    public:
    inline /*constexpr */ void operator()() const
    {
    }
    
    
  };
  
  r.wait().handle<int, __lambda_40_22>(__lambda_40_22{}).handle<double, __lambda_41_25>(__lambda_41_25{});
  return 0;
}

 

 

+ Recent posts