<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>Sonji-log</title>
    <link>https://sonlog.tistory.com/</link>
    <description>개발 공부 내용을 메모합니다.
질문 및 지적사항은 언제나 환영합니다.</description>
    <language>ko</language>
    <pubDate>Sat, 13 Jun 2026 07:36:10 +0900</pubDate>
    <generator>TISTORY</generator>
    <ttl>100</ttl>
    <managingEditor>Sonji</managingEditor>
    <item>
      <title>[Effective C++] Cp 8. 소멸자 밖으로 예외가 빠져나가지 않게 하자</title>
      <link>https://sonlog.tistory.com/entry/Effective-C-Cp-8-%EC%86%8C%EB%A9%B8%EC%9E%90-%EB%B0%96%EC%9C%BC%EB%A1%9C-%EC%98%88%EC%99%B8%EA%B0%80-%EB%B9%A0%EC%A0%B8%EB%82%98%EA%B0%80%EC%A7%80-%EC%95%8A%EA%B2%8C-%ED%95%98%EC%9E%90</link>
      <description>&lt;h2&gt;1. 이 항목이 왜 중요한가&lt;/h2&gt;
&lt;p&gt;소멸자(destructor)는 객체의 마지막 정리 담당자다. 메모리를 해제하고, 파일을 닫고, mutex를 풀고, 데이터베이스 연결을 종료한다. 앞서 Cp 7.에서 소멸자가 제대로 호출되는지가 중요하다고 봤다면, 이번에는 &lt;strong&gt;호출된 소멸자가 어떤 식으로 끝나야 하는가&lt;/strong&gt;를 다룬다.&lt;/p&gt;
&lt;p&gt;겉보기에는 이런 코드가 자연스럽다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class DBConnection
{
public:
    static DBConnection create();

    void close();  // 실패하면 예외를 던질 수 있다
};

class DBConn
{
public:
    explicit DBConn(const DBConnection&amp;amp; connection)
        : db(connection)
    {}

    ~DBConn()
    {
        db.close();   // 여기서 예외가 날 수 있다
    }

private:
    DBConnection db;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;DBConn&lt;/code&gt;은 &lt;code&gt;DBConnection&lt;/code&gt;을 감싸는 RAII 클래스처럼 보인다. 객체가 사라질 때 데이터베이스 연결도 자동으로 닫아주니 좋아 보인다.&lt;/p&gt;
&lt;p&gt;그런데 &lt;code&gt;close()&lt;/code&gt;가 실패해서 예외를 던지면 무슨 일이 벌어질까?&lt;/p&gt;
&lt;p&gt;평범한 멤버 함수라면 호출자에게 예외를 전달하면 된다. 하지만 소멸자는 사용자가 직접 부르는 경우보다 scope를 벗어나거나 다른 예외를 처리하는 도중에 자동으로 호출되는 경우가 많다. 이때 소멸자 밖으로 예외가 새어 나가면 프로그램은 더 이상 정상적으로 정리 흐름을 이어가기 어렵다.&lt;/p&gt;
&lt;p&gt;핵심은 간단하다.&lt;strong&gt;소멸자 밖으로 예외가 빠져나가게 두면 안 된다. 소멸자는 예외를 삼키든, 프로그램을 끝내든, 밖으로 흘려보내지 않아야 한다.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;2. 문제 상황을 코드로 보자&lt;/h2&gt;
&lt;h3&gt;2-1. 컨테이너가 객체들을 정리하는 경우&lt;/h3&gt;
&lt;p&gt;소멸자는 우리가 직접 &lt;code&gt;delete&lt;/code&gt;를 쓸 때 뿐 아니라, 지역 객체가 scope를 벗어날 때, 컨테이너가 내부 원소를 지울 때, 예외가 발생해서 stack unwinding(이게뭘까? : &lt;a href=&quot;https://luckygg.tistory.com/372)%EC%9D%B4&quot;&gt;https://luckygg.tistory.com/372)이&lt;/a&gt; 진행될 때도 자동으로 호출된다.&lt;/p&gt;
&lt;p&gt;예를 들어 &lt;code&gt;DBConn&lt;/code&gt; 객체 여러 개를 &lt;code&gt;std::vector&lt;/code&gt;에 넣었다고 해보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::vector&amp;lt;DBConn&amp;gt; connections;

// ... 여러 DBConn 객체 사용 ...

// vector가 파괴될 때, 내부 DBConn들의 소멸자가 차례로 호출된다&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;vector&lt;/code&gt;가 파괴되면서 원소들의 소멸자를 부른다. 그런데 첫 번째 &lt;code&gt;DBConn&lt;/code&gt;의 소멸자에서 &lt;code&gt;close()&lt;/code&gt;가 예외를 던졌다고 해보자. 이미 컨테이너는 자기 정리 작업 중이다.&lt;/p&gt;
&lt;p&gt;더 나쁜 경우도 있다. 어떤 함수가 이미 예외를 던져서 stack unwinding이 진행되는 중인데, 그 과정에서 지역 객체의 소멸자가 호출되고, 그 소멸자까지 또 예외를 던지는 경우다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void run()
{
    DBConn dbc(DBConnection::create());

    // ...

    throw std::runtime_error(&amp;quot;작업 실패&amp;quot;);

    // dbc는 stack unwinding 중에 파괴된다
    // 이때 ~DBConn()에서도 예외가 나오면 상황이 무너진다
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;C++은 동시에 여러 예외가 겹쳐 밖으로 전파되는 상황을 안전하게 이어갈 수 없다. 특히 예외 처리 도중 소멸자에서 또 다른 예외가 빠져나가면, 프로그램은 보통 &lt;code&gt;std::terminate&lt;/code&gt;로 끝난다.&lt;/p&gt;
&lt;p&gt;C++11 이후에는 여기에 한 가지가 더 붙는다. 소멸자는 대체로 암묵적으로 &lt;code&gt;noexcept&lt;/code&gt;로 간주되기 때문에, 소멸자 밖으로 예외가 나가면 그 자체로 &lt;code&gt;std::terminate&lt;/code&gt;가 호출될 수 있다.&lt;/p&gt;
&lt;p&gt;말하자면 소멸자는 &amp;quot;실패할 수 없는 곳&amp;quot;이 아니라, &lt;strong&gt;실패를 밖으로 보고하기 어려운 곳&lt;/strong&gt;이다.&lt;/p&gt;
&lt;h2&gt;3. 해결 방법 1: 소멸자 안에서 끝장을 본다&lt;/h2&gt;
&lt;h3&gt;3-1. 실패하면 프로그램을 종료한다&lt;/h3&gt;
&lt;p&gt;첫 번째 선택지는 예외를 잡고, 프로그램을 끝내는 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class DBConn
{
public:
    explicit DBConn(const DBConnection&amp;amp; connection)
        : db(connection)
    {}

    ~DBConn()
    {
        try
        {
            db.close();
        }
        catch (...)
        {
            // 로그를 남길 수 있다면 남긴다
            std::abort();
        }
    }

private:
    DBConnection db;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 방식은 거칠어 보인다. 하지만 어떤 시스템에서는 닫기 실패가 프로그램 상태를 더 이상 믿을 수 없게 만든다는 뜻일 수 있다. 그럴 때는 어설프게 계속 진행하는 것보다 명시적으로 완전히 죽는 편이 낫다.&lt;/p&gt;
&lt;p&gt;중요한 건 &lt;code&gt;close()&lt;/code&gt;의 예외가 소멸자 밖으로 나가지 않는다는 점이다. 소멸자가 책임지고 흐름을 끊는다.&lt;/p&gt;
&lt;h3&gt;3-2. 실패를 삼킨다&lt;/h3&gt;
&lt;p&gt;두 번째 선택지는 예외를 잡고 삼키는 것이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class DBConn
{
public:
    explicit DBConn(const DBConnection&amp;amp; connection)
        : db(connection)
    {}

    ~DBConn()
    {
        try
        {
            db.close();
        }
        catch (...)
        {
            // 가능하면 로그를 남긴다
            // 예외는 소멸자 밖으로 내보내지 않는다
        }
    }

private:
    DBConnection db;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 방법도 완벽하지 않다. &lt;code&gt;close()&lt;/code&gt; 실패는 실제 문제일 수 있는데, 그냥 묻어버리면 사용자는 아무 일도 없었던 것처럼 다음 단계로 넘어간다.&lt;/p&gt;
&lt;p&gt;그래도 &amp;quot;소멸자에서 예외가 밖으로 나가 프로그램이 갑자기 종료되는 것&amp;quot;보다는 나을 때가 있다. 특히 정리 작업 실패가 치명적이지 않고, 로그만으로 충분히 추적 가능한 경우라면 현실적인 선택이다.&lt;/p&gt;
&lt;p&gt;다만 이 방식에는 전제가 있다. &lt;strong&gt;예외를 삼키려면, 정말 삼켜도 되는 실패인지 먼저 판단해야 한다.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;4. 해결 방법 2: 실패를 사용자가 처리할 수 있게 한다&lt;/h2&gt;
&lt;h3&gt;4-1. close를 명시적으로 제공하자&lt;/h3&gt;
&lt;p&gt;소멸자에서 실패를 보고하기 어렵다면, 실패를 보고할 수 있는 일반 멤버 함수에서 일을 하게 만들면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class DBConn
{
public:
    explicit DBConn(const DBConnection&amp;amp; connection)
        : db(connection)
    {}

    void close()
    {
        db.close();      // 실패하면 호출자에게 예외를 전달한다
        closed = true;
    }

    ~DBConn()
    {
        if (!closed)
        {
            try
            {
                db.close();   // 사용자가 안 닫았을 때의 마지막 안전망
            }
            catch (...)
            {
                // 여기서는 예외를 밖으로 내보내지 않는다
            }
        }
    }

private:
    DBConnection db;
    bool closed = false;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이제 사용자는 연결 닫기 실패에 반응하고 싶을 때 직접 &lt;code&gt;close()&lt;/code&gt;를 호출할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;DBConn dbc(DBConnection::create());

// ... dbc 사용 ...

dbc.close();   // 실패하면 여기서 예외를 처리할 수 있다&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 구조에서는 책임이 더 분명해진다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;사용자가 실패를 처리하고 싶다 → &lt;code&gt;close()&lt;/code&gt;를 직접 호출한다.&lt;/li&gt;
&lt;li&gt;사용자가 호출하지 않았다 → 소멸자가 마지막 안전망으로 정리한다.&lt;/li&gt;
&lt;li&gt;소멸자에서 실패했다 → 예외를 밖으로 내보내지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4-2. 소멸자는 진짜 오류 처리 장소가 아니다&lt;/h3&gt;
&lt;p&gt;소멸자는 객체가 사라지는 마지막 순간에 불린다. 이때는 호출자가 무엇을 하려던 중인지 알기 어렵다. 정상 흐름일 수도 있고, 이미 예외를 처리하는 중일 수도 있다. 그래서 소멸자에서 &amp;quot;실패를 호출자에게 알려주자&amp;quot;는 설계는 대체로 불안하다.&lt;/p&gt;
&lt;p&gt;반대로 일반 멤버 함수는 호출자가 의도를 가지고 부른다. &lt;code&gt;close()&lt;/code&gt;가 실패하면 그 자리에서 재시도할지, 로그를 남길지, 사용자에게 알릴지 결정할 수 있다.&lt;/p&gt;
&lt;p&gt;그러니 실패에 의미 있게 대응해야 하는 작업이라면, 소멸자에만 숨겨두지 않는 편이 좋다.&lt;/p&gt;
&lt;h2&gt;5. RAII와 모순되는 말은 아니다&lt;/h2&gt;
&lt;p&gt;여기서 헷갈릴 수 있다. &amp;quot;RAII라면 자원 해제를 소멸자에 맡기라고 하지 않았나? 그런데 이제 소멸자에서 닫지 말라는 건가?&amp;quot;&lt;/p&gt;
&lt;p&gt;RAII의 핵심은 자원의 생명주기를 객체 생명주기에 묶는 것이다. 따라서 소멸자는 여전히 마지막 정리 책임을 가져야 한다. 다만 &lt;strong&gt;실패를 밖으로 던지는 방식으로 보고하면 안 된다&lt;/strong&gt;는 뜻이다.&lt;/p&gt;
&lt;p&gt;좋은 RAII 클래스는 보통 이렇게 행동한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;소멸자에서 자원을 반드시 정리하려고 시도한다.&lt;/li&gt;
&lt;li&gt;소멸자 밖으로 예외를 내보내지는 않는다.&lt;/li&gt;
&lt;li&gt;사용자가 실패를 처리해야 한다면 별도의 명시적 함수(&lt;code&gt;close&lt;/code&gt;, &lt;code&gt;commit&lt;/code&gt;, &lt;code&gt;flush&lt;/code&gt; 등)를 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;예를 들어 파일 출력 버퍼를 비우는 &lt;code&gt;flush()&lt;/code&gt;, 트랜잭션을 확정하는 &lt;code&gt;commit()&lt;/code&gt;, 네트워크 연결을 닫는 &lt;code&gt;close()&lt;/code&gt; 같은 작업은 실패가 의미를 가진다. 이런 작업은 소멸자에만 맡기면 호출자가 실패를 다룰 기회를 잃는다.&lt;/p&gt;
&lt;p&gt;핵심은 &amp;quot;소멸자에서 아무것도 하지 말라&amp;quot;가 아니다. 소멸자는 마지막 안전망이어야지, 예외를 통해 오류를 보고하는 주 통로가 되어서는 안 된다.&lt;/p&gt;
&lt;h2&gt;6. 현대 C++에서는 더 엄격하다&lt;/h2&gt;
&lt;p&gt;Effective C++가 쓰이던 시기(3판은 2005년에 집필되고 있었다)에는 소멸자에서 예외가 빠져나갈 때의 위험을 주로 &amp;quot;예외가 겹치면 프로그램이 종료된다&amp;quot;는 관점으로 설명했다.&lt;/p&gt;
&lt;p&gt;현대 C++에서는 여기에 &lt;code&gt;noexcept&lt;/code&gt;까지 함께 생각해야 한다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;noexcept&lt;/code&gt;는 말 그대로 &amp;quot;이 함수는 예외를 밖으로 던지지 않는다&amp;quot;는 약속이다. 함수 선언 뒤에 붙일 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void cleanup() noexcept;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 약속이 깨지면 어떻게 될까? &lt;code&gt;noexcept&lt;/code&gt; 함수 안에서 예외가 발생하는 것 자체가 문제는 아니다. 그 예외가 함수 안에서 잡히면 괜찮다. 하지만 예외가 함수 밖으로 빠져나가려는 순간, C++ 런타임은 더 이상 일반적인 예외 전파를 계속하지 않고 &lt;code&gt;std::terminate&lt;/code&gt;를 호출한다.&lt;/p&gt;
&lt;p&gt;소멸자가 여기에 특히 민감하다. C++11 이후 소멸자는 특별히 지정하지 않으면 대체로 &lt;code&gt;noexcept(true)&lt;/code&gt;로 취급된다. 즉, 아래 코드는 보기보다 더 위험하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class DBConn
{
public:
    explicit DBConn(const DBConnection&amp;amp; connection)
        : db(connection)
    {}

    ~DBConn()
    {
        db.close();   // close가 던지면 std::terminate가 호출될 수 있다
    }

private:
    DBConnection db;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;물론 &lt;code&gt;~DBConn() noexcept(false)&lt;/code&gt;처럼 소멸자가 예외를 던질 수 있다고 선언하는 방법도 있다. 하지만 그건 아주 조심스럽게 써야 한다. 컨테이너, 스마트 포인터, stack unwinding과 만나는 순간 여전히 위험한 설계가 되기 쉽다.&lt;/p&gt;
&lt;p&gt;실무적으로는 이쪽이 더 단순하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class DBConn
{
public:
    explicit DBConn(const DBConnection&amp;amp; connection)
        : db(connection)
    {}

    ~DBConn() noexcept
    {
        try
        {
            if (!closed)
            {
                db.close();
            }
        }
        catch (...)
        {
            // 로그 또는 종료 정책
        }
    }

    void close()
    {
        db.close();
        closed = true;
    }

private:
    DBConnection db;
    bool closed = false;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;소멸자는 &lt;code&gt;noexcept&lt;/code&gt;라는 의도를 분명히 갖고, 실패 처리는 소멸자 내부 정책으로 끝낸다. 사용자가 실패를 직접 다뤄야 하면 일반 멤버 함수를 호출하게 한다.&lt;/p&gt;
&lt;h2&gt;7. 실무에서는 어떻게 판단하면 좋을까&lt;/h2&gt;
&lt;p&gt;소멸자에서 어떤 함수를 호출하려고 할 때는 몇 가지를 자문해보면 좋다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이 함수가 예외를 던질 수 있는가?&lt;/li&gt;
&lt;li&gt;실패했을 때 호출자가 실제로 대응해야 하는가?&lt;/li&gt;
&lt;li&gt;소멸자에서 실패를 삼켜도 시스템 상태가 괜찮은가?&lt;/li&gt;
&lt;li&gt;삼키면 안 되는 실패라면, 프로그램을 종료하는 것이 더 정직한가?&lt;/li&gt;
&lt;li&gt;사용자가 명시적으로 호출할 수 있는 &lt;code&gt;close()&lt;/code&gt;나 &lt;code&gt;commit()&lt;/code&gt; 같은 함수를 제공해야 하는가?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;내 코드도 그렇지만, &amp;quot;소멸자가 알아서 해주겠지&amp;quot;라는 생각으로 정리 작업을 전부 몰아넣으면 오류 처리 설계가 흐려진다. 자동 정리는 좋다. 하지만 실패까지 자동으로 잘 처리되는 건 아니니 신경을 좀 써줘야 한다.&lt;/p&gt;
&lt;h2&gt;8. 항목 8 핵심 정리&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;반드시 기억할 것&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;소멸자 밖으로 예외가 빠져나가게 두면 안 된다. 소멸자는 예외를 잡아서 처리해야 한다.&lt;/li&gt;
&lt;li&gt;소멸자 안에서 호출하는 함수가 예외를 던질 수 있다면, 소멸자는 그 예외를 잡고 삼키거나 프로그램을 종료해야 한다.&lt;/li&gt;
&lt;li&gt;사용자가 어떤 작업의 실패에 대응해야 한다면, 그 작업은 소멸자가 아니라 일반 멤버 함수에서 수행할 수 있게 제공해야 한다.&lt;/li&gt;
&lt;li&gt;소멸자는 자원 정리의 마지막 안전망으로 두고, 오류 보고의 주 통로로 쓰지 않는다.&lt;/li&gt;
&lt;li&gt;현대 C++에서는 소멸자가 대체로 &lt;code&gt;noexcept&lt;/code&gt;라는 점까지 고려해야 한다. 소멸자에서 예외가 나가면 &lt;code&gt;std::terminate&lt;/code&gt;로 이어질 수 있다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>책/Effective C++</category>
      <author>Sonji</author>
      <guid isPermaLink="true">https://sonlog.tistory.com/18</guid>
      <comments>https://sonlog.tistory.com/entry/Effective-C-Cp-8-%EC%86%8C%EB%A9%B8%EC%9E%90-%EB%B0%96%EC%9C%BC%EB%A1%9C-%EC%98%88%EC%99%B8%EA%B0%80-%EB%B9%A0%EC%A0%B8%EB%82%98%EA%B0%80%EC%A7%80-%EC%95%8A%EA%B2%8C-%ED%95%98%EC%9E%90#entry18comment</comments>
      <pubDate>Wed, 27 May 2026 08:59:19 +0900</pubDate>
    </item>
    <item>
      <title>[Effective C++] Cp 7. 다형성을 가진 기본 클래스에서는 소멸자를 반드시 가상 소멸자로 선언하자</title>
      <link>https://sonlog.tistory.com/entry/Effective-C-Cp-7-%EB%8B%A4%ED%98%95%EC%84%B1%EC%9D%84-%EA%B0%80%EC%A7%84-%EA%B8%B0%EB%B3%B8-%ED%81%B4%EB%9E%98%EC%8A%A4%EC%97%90%EC%84%9C%EB%8A%94-%EC%86%8C%EB%A9%B8%EC%9E%90%EB%A5%BC-%EB%B0%98%EB%93%9C%EC%8B%9C-%EA%B0%80%EC%83%81-%EC%86%8C%EB%A9%B8%EC%9E%90%EB%A1%9C-%EC%84%A0%EC%96%B8%ED%95%98%EC%9E%90</link>
      <description>&lt;h2&gt;1. 이 항목이 왜 중요한가&lt;/h2&gt;
&lt;p&gt;C++에서는 기본 클래스(base class) 포인터로 파생 클래스(derived class) 객체를 다루는 일이 아주 흔하다. 다형성(polymorphism)을 쓰는 코드라면 거의 항상 이런 형태가 나온다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;Base* p = new Derived();
// ...
delete p;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;겉보기에는 꽤 자연스럽지만, 여기에는 함정이 하나 숨어 있다.&lt;/p&gt;
&lt;p&gt;만약 &lt;code&gt;Base&lt;/code&gt;의 소멸자가 가상 함수(virtual function)가 아니라면, &lt;code&gt;delete p&lt;/code&gt;를 했을 때 무슨 일이 벌어질까?&lt;/p&gt;
&lt;p&gt;결론부터 말하면 &lt;strong&gt;정의되지 않은 동작(undefined behavior)이 수행된다&lt;/strong&gt;. 보통은 다음과 같은 일이 일어난다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;객체의 기본 클래스 부분(&lt;code&gt;Base&lt;/code&gt;)은 소멸된다.&lt;/li&gt;
&lt;li&gt;하지만 파생 클래스 부분(&lt;code&gt;Derived&lt;/code&gt;)은 소멸되지 않는다.&lt;/li&gt;
&lt;li&gt;결과적으로 객체가 &amp;quot;반쯤만 파괴된&amp;quot; 상태가 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;파생 클래스 쪽에서 잡고 있던 메모리, 파일 핸들, 소켓 같은 자원이 있었다면 그대로 새어 나간다.&lt;/p&gt;
&lt;p&gt;이런 문제를 방지하려면, 기본 클래스 포인터로 파생 객체를 &lt;code&gt;delete&lt;/code&gt; 할 때, 그 기본 클래스의 소멸자는 반드시 &lt;code&gt;virtual&lt;/code&gt;이어야 한다.&lt;/p&gt;
&lt;h2&gt;2. 문제 상황을 코드로 보자&lt;/h2&gt;
&lt;h3&gt;2-1. 시간 기록 클래스 예시&lt;/h3&gt;
&lt;p&gt;시간을 재는 클래스 계층을 만든다고 해보자. 추상적인 &amp;quot;시간 기록기&amp;quot;가 있고, 그 아래에 구현 방식이 다른 여러 종류가 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class TimeKeeper
{
public:
    TimeKeeper();
    ~TimeKeeper();   // 주의: 가상 소멸자가 아니다
};

class AtomicClock : public TimeKeeper { /* ... */ };  // 원자 시계
class WaterClock  : public TimeKeeper { /* ... */ };  // 물 시계
class WristWatch  : public TimeKeeper { /* ... */ };  // 손목 시계&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;사용하는 쪽에서는 구체적으로 어떤 시계인지 신경 쓰고 싶지 않다. 그냥 &amp;quot;시간 기록기 하나 주세요&amp;quot;라고 말하고 싶다. 그래서 보통 팩토리 함수(factory function)를 쓴다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 호출하는 쪽은 구체 타입을 모른 채, 기본 클래스 포인터만 받는다
TimeKeeper* getTimeKeeper();&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 함수는 내부에서 &lt;code&gt;AtomicClock&lt;/code&gt;이든 &lt;code&gt;WaterClock&lt;/code&gt;이든 적당한 파생 객체를 &lt;code&gt;new&lt;/code&gt;로 만들어서, &lt;code&gt;TimeKeeper*&lt;/code&gt;로 돌려준다.&lt;/p&gt;
&lt;h3&gt;2-2. delete 하는 순간 문제가 터진다&lt;/h3&gt;
&lt;p&gt;팩토리 함수가 힙(heap)에 객체를 만들어 줬으니, 다 쓰고 나면 &lt;code&gt;delete&lt;/code&gt; 해줘야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;TimeKeeper* ptk = getTimeKeeper();  // 실제로는 파생 객체를 가리킨다

// ... ptk 사용 ...

delete ptk;   // 여기서 문제 발생&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;ptk&lt;/code&gt;의 정적 타입은 &lt;code&gt;TimeKeeper*&lt;/code&gt;지만, 실제 가리키는 대상은 &lt;code&gt;AtomicClock&lt;/code&gt; 같은 파생 객체다.&lt;/p&gt;
&lt;p&gt;그런데 &lt;code&gt;TimeKeeper&lt;/code&gt;의 소멸자가 &lt;code&gt;virtual&lt;/code&gt;이 아니므로, &lt;code&gt;delete&lt;/code&gt;는 &amp;quot;정적 타입인 &lt;code&gt;TimeKeeper&lt;/code&gt;의 소멸자만&amp;quot; 부르려고 한다. 그 결과 파생 클래스 부분은 정리되지 않고 남는다.&lt;/p&gt;
&lt;p&gt;말 그대로 객체가 반쪽만 파괴되는 셈이다. 자원 누수(leak)와 자료구조 오염으로 이어지기 딱 좋은 상황이다.&lt;/p&gt;
&lt;h2&gt;3. 해결 방법: 기본 클래스 소멸자를 가상으로&lt;/h2&gt;
&lt;h3&gt;3-1. virtual 소멸자 하나면 끝&lt;/h3&gt;
&lt;p&gt;해결책은 의외로 단순하다. 기본 클래스의 소멸자에 &lt;code&gt;virtual&lt;/code&gt;만 붙이면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class TimeKeeper
{
public:
    TimeKeeper();
    virtual ~TimeKeeper();   // 가상 소멸자
};

TimeKeeper* ptk = getTimeKeeper();

// ...

delete ptk;   // 이제 올바르게 동작한다&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 해두면 &lt;code&gt;delete ptk&lt;/code&gt;가 실제 객체의 타입(예: &lt;code&gt;AtomicClock&lt;/code&gt;)에 맞는 소멸자를 먼저 부르고, 이어서 기본 클래스 소멸자까지 차례로 호출하기 때문에 객체 전체가 제대로 파괴된다.&lt;/p&gt;
&lt;h3&gt;3-2. 어떤 클래스에 가상 소멸자가 필요한가&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;가상 함수를 하나라도 가진 클래스라면, 소멸자도 거의 항상 가상이어야 한다.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;가상 함수가 있다는 건 &amp;quot;이 클래스는 파생되어 다형적으로 쓰일 의도&amp;quot;라는 뜻이다. 그렇다면 기본 클래스 포인터로 객체를 &lt;code&gt;delete&lt;/code&gt; 할 가능성이 있다는 의미이고, 따라서 소멸자도 가상이어야 앞뒤가 맞는다.&lt;/p&gt;
&lt;h2&gt;4. 그렇다고 모든 클래스에 virtual을 붙이면 안 된다&lt;/h2&gt;
&lt;p&gt;여기서 흔히 하는 오해가 있다. &amp;quot;그럼 안전하게 모든 소멸자를 가상으로 만들면 되지 않나?&amp;quot;&lt;/p&gt;
&lt;p&gt;가상 함수는 공짜가 아니다.&lt;/p&gt;
&lt;h3&gt;4-1. vptr이라는 비용&lt;/h3&gt;
&lt;p&gt;클래스에 가상 함수가 하나라도 생기면, 객체마다 보통 &lt;strong&gt;가상 함수 테이블 포인터(vptr, virtual table pointer)&lt;/strong&gt; 가 따라붙는다. 그만큼 객체 크기가 커진다.&lt;/p&gt;
&lt;p&gt;예를 들어 좌표를 표현하는 단순한 클래스를 생각해 보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Point
{
private:
    int x, y;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;int&lt;/code&gt;가 32비트라면 이 객체는 64비트면 충분하다. 다른 언어나 C 구조체와 메모리 배치가 그대로 호환되기도 한다.&lt;/p&gt;
&lt;p&gt;그런데 여기에 가상 함수(가상 소멸자 포함)를 하나 넣는 순간, vptr이 추가되면서 객체가 커진다. 32비트 환경이면 96비트, 64비트 포인터 환경이면 128비트로 늘어날 수 있다.&lt;/p&gt;
&lt;h3&gt;4-2. 호환성 문제&lt;/h3&gt;
&lt;p&gt;vptr이 끼면 객체의 메모리 배치가 &amp;quot;순수한 데이터 묶음&amp;quot;에서 벗어난다. 그러면 C로 짠 코드나 다른 언어와 같은 메모리 표현을 주고받기 어려워진다. 즉, 외부와 비트 단위로 호환되어야 하는 타입에 함부로 가상 함수를 넣으면 곤란하다.&lt;/p&gt;
&lt;p&gt;그래서 정리하면 이렇다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;다형적으로 쓸 기본 클래스다&lt;/strong&gt; → 소멸자를 가상으로.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;기본 클래스로 쓸 의도가 없거나, 다형적으로 다룰 일이 없다&lt;/strong&gt; → 굳이 가상 소멸자를 붙이지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5. 표준 컨테이너를 상속하지 말자&lt;/h2&gt;
&lt;p&gt;이 항목에서 자연스럽게 따라오는 주의사항이 하나 있다.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;std::string&lt;/code&gt;, &lt;code&gt;std::vector&lt;/code&gt;, &lt;code&gt;std::list&lt;/code&gt;, &lt;code&gt;std::set&lt;/code&gt; 같은 표준 라이브러리 타입들은 &lt;strong&gt;소멸자가 가상이 아니다.&lt;/strong&gt; 이들은 애초에 기본 클래스로 쓰라고 설계된 타입이 아니기 때문이다.&lt;/p&gt;
&lt;p&gt;그래서 아래 같은 코드는 위험하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class SpecialString : public std::string  // 위험한 발상
{
    // ...
};&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;std::string* ps = new SpecialString(/* ... */);
delete ps;   // string의 소멸자가 비가상이므로, SpecialString 부분이 누락된다&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;std::string&lt;/code&gt;처럼 비가상 소멸자를 가진 클래스를 상속해서, 기본 클래스 포인터로 &lt;code&gt;delete&lt;/code&gt; 하면 항목 7에서 본 그 문제가 똑같이 재현된다. 그러니 비가상 소멸자를 가진 클래스, 그리고 애초에 기본 클래스로 설계되지 않은 클래스는 상속 대상으로 삼지 않는 편이 안전하다.&lt;/p&gt;
&lt;h2&gt;6. 순수 가상 소멸자라는 기법&lt;/h2&gt;
&lt;p&gt;가끔 추상 클래스(abstract class)로 만들고 싶은데, 마땅히 순수 가상 함수로 둘 멤버가 없는 경우가 있다. 추상 클래스는 순수 가상 함수(pure virtual function)를 하나라도 가져야 만들어지는데, 딱히 그럴 함수가 없을 때다.&lt;/p&gt;
&lt;p&gt;이럴 때 쓸 수 있는 약간의 트릭이 &lt;strong&gt;순수 가상 소멸자(pure virtual destructor)&lt;/strong&gt; 다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class AWOV   // Abstract Without Virtuals: 가상 함수 없는 추상 클래스
{
public:
    virtual ~AWOV() = 0;   // 순수 가상 소멸자
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 하면 &lt;code&gt;AWOV&lt;/code&gt;는 추상 클래스가 되어 직접 인스턴스를 만들 수 없게 된다. 동시에 소멸자가 가상이니 다형적 삭제도 안전해진다.&lt;/p&gt;
&lt;h3&gt;6-1. 단, 정의는 반드시 제공해야 한다&lt;/h3&gt;
&lt;p&gt;여기서 함정이 하나 있다. 순수 가상 소멸자는 &lt;strong&gt;선언만으로는 안 되고, 정의(구현)까지 반드시 제공해야 한다.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;AWOV::~AWOV() {}   // 비어 있어도 좋으니, 정의는 꼭 있어야 한다&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이유는 소멸 과정의 동작 방식 때문이다. 파생 클래스의 소멸자가 끝나면, 그 다음으로 기본 클래스의 소멸자가 자동으로 호출된다. 즉 &lt;code&gt;AWOV&lt;/code&gt;의 소멸자는 실제로 불리게 되어 있고, 정의가 없으면 링크 단계에서 에러가 난다.&lt;/p&gt;
&lt;p&gt;앞 cp.6에서 봤던 &amp;quot;선언만 하고 정의는 안 한다&amp;quot;와는 정반대 상황이라, 헷갈리기 쉬운 부분이다. 그쪽은 호출될 일이 없게 막는 게 목적이었고, 이쪽은 반드시 호출되므로 정의가 필요하다.&lt;/p&gt;
&lt;h2&gt;7. &amp;quot;기본 클래스&amp;quot;라고 다 가상 소멸자가 필요한 건 아니다&lt;/h2&gt;
&lt;p&gt;마지막으로 짚어둘 점. 이 항목의 규칙은 어디까지나 &lt;strong&gt;다형성을 가진 기본 클래스&lt;/strong&gt;에만 적용된다.&lt;/p&gt;
&lt;p&gt;세상의 모든 기본 클래스가 다형적으로 쓰이는 건 아니다. 어떤 기본 클래스는 다형적 삭제를 의도하지 않는다. 예를 들어 항목 6에서 본 &lt;code&gt;Uncopyable&lt;/code&gt; 같은 클래스는, 복사 금지라는 기능을 물려주려고 만든 도구일 뿐이지 &amp;quot;기본 클래스 포인터로 다뤄지는 객체&amp;quot;를 만들려는 게 아니다. 표준 라이브러리의 &lt;code&gt;input_iterator_tag&lt;/code&gt; 같은 타입들도 비슷하다.&lt;/p&gt;
&lt;p&gt;이런 클래스들은 다형적으로 &lt;code&gt;delete&lt;/code&gt; 될 일이 없으므로, 가상 소멸자가 필요 없다.&lt;/p&gt;
&lt;p&gt;그러니 기계적으로 &amp;quot;기본 클래스니까 무조건 가상 소멸자&amp;quot;가 아니라, &lt;strong&gt;&amp;quot;이 객체를 기본 클래스 포인터로 받아서 delete 할 일이 있는가?&amp;quot;&lt;/strong&gt; 를 기준으로 판단하는 게 맞다.&lt;/p&gt;
&lt;h2&gt;8. 항목 7 핵심 정리&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;반드시 기억할 것&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;다형성을 가진 기본 클래스, 즉 기본 클래스 포인터/참조로 파생 객체를 다루는 클래스에는 반드시 가상 소멸자를 선언해야 한다.&lt;/li&gt;
&lt;li&gt;어떤 클래스가 가상 함수를 하나라도 가지고 있다면, 그 클래스의 소멸자도 가상이어야 한다.&lt;/li&gt;
&lt;li&gt;기본 클래스로 쓸 의도가 없거나 다형적으로 다룰 일이 없는 클래스에는 가상 소멸자를 붙이지 않는다. (vptr로 인한 크기 증가와 호환성 손실 때문)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;std::string&lt;/code&gt;이나 표준 컨테이너처럼 비가상 소멸자를 가진 타입은 상속 대상으로 삼지 않는다.&lt;/li&gt;
&lt;li&gt;순수 가상 소멸자로 추상 클래스를 만들 수 있지만, 이때는 그 소멸자의 정의를 반드시 제공해야 한다.&lt;/li&gt;
&lt;/ul&gt;</description>
      <category>책/Effective C++</category>
      <author>Sonji</author>
      <guid isPermaLink="true">https://sonlog.tistory.com/17</guid>
      <comments>https://sonlog.tistory.com/entry/Effective-C-Cp-7-%EB%8B%A4%ED%98%95%EC%84%B1%EC%9D%84-%EA%B0%80%EC%A7%84-%EA%B8%B0%EB%B3%B8-%ED%81%B4%EB%9E%98%EC%8A%A4%EC%97%90%EC%84%9C%EB%8A%94-%EC%86%8C%EB%A9%B8%EC%9E%90%EB%A5%BC-%EB%B0%98%EB%93%9C%EC%8B%9C-%EA%B0%80%EC%83%81-%EC%86%8C%EB%A9%B8%EC%9E%90%EB%A1%9C-%EC%84%A0%EC%96%B8%ED%95%98%EC%9E%90#entry17comment</comments>
      <pubDate>Tue, 26 May 2026 11:39:07 +0900</pubDate>
    </item>
    <item>
      <title>[Effective C++] Cp 6. 컴파일러가 만들어주는 함수를 원치 않으면 사용 금지시키자</title>
      <link>https://sonlog.tistory.com/entry/Effective-C-Cp-6-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC%EA%B0%80-%EB%A7%8C%EB%93%A4%EC%96%B4%EC%A3%BC%EB%8A%94-%ED%95%A8%EC%88%98%EB%A5%BC-%EC%9B%90%EC%B9%98-%EC%95%8A%EC%9C%BC%EB%A9%B4-%EC%82%AC%EC%9A%A9-%EA%B8%88%EC%A7%80%EC%8B%9C%ED%82%A4%EC%9E%90</link>
      <description>&lt;h2&gt;1. 이 항목이 왜 중요한가&lt;/h2&gt;
&lt;p&gt;C++에서는 내가 직접 쓰지 않아도 컴파일러가 몇몇 함수를 자동으로 만들어 주는 경우가 있다. 대표적으로 아래 두 가지가 자주 문제를 만든다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;복사 생성자(copy constructor)&lt;/li&gt;
&lt;li&gt;복사 대입 연산자(copy assignment operator)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;겉으로 보기에는 편리하다. 하지만 어떤 클래스는 &lt;strong&gt;&amp;quot;복사되면 안 되는 객체&amp;quot;&lt;/strong&gt; 인데도, 아무 조치를 하지 않으면 복사가 가능해질 수 있다.&lt;/p&gt;
&lt;p&gt;예를 들어, 아래와 같은 경우를 생각해 보자.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;mutex 같은 동기화 객체&lt;/li&gt;
&lt;li&gt;파일 핸들, 소켓 핸들 같은 시스템 자원&lt;/li&gt;
&lt;li&gt;&amp;quot;오직 하나만 존재해야 하는&amp;quot; 객체(Singleton Pattern에 해당하는 성격의 객체를 생각하면 쉽다)&lt;/li&gt;
&lt;li&gt;내부적으로 복사 의미가 애매하거나 위험한 객체&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이런 객체를 실수로 복사하면 다음과 같은 문제가 생길 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;같은 자원을 여러 객체가 동시에 가리킨다.&lt;/li&gt;
&lt;li&gt;누가 자원을 해제해야 하는지 애매해진다.&lt;/li&gt;
&lt;li&gt;이중 해제(double free), 자원 누수(leak), 논리 오류가 생긴다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;핵심은 간단하다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;복사가 말이 안 되는 클래스는 아예 복사 자체를 막아야 한다.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&amp;quot;아마 안 쓰겠지&amp;quot;가 아니라, 컴파일 단계에서 못 쓰게 만들어야 한다.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;2. 자동 생성 함수는 항상 반가운 것이 아니다&lt;/h2&gt;
&lt;h3&gt;2-1. 컴파일러는 빈칸을 채워 준다&lt;/h3&gt;
&lt;p&gt;클래스를 만들었는데 복사 생성자나 복사 대입 연산자를 직접 선언하지 않으면,&lt;br&gt;컴파일러가 필요에 따라 이 함수들을 만들어 줄 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Widget
{
public:
    Widget() {}
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;겉보기에는 생성자 하나만 있는 클래스처럼 보이지만, 실제로는 복사 관련 함수도 생길 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;Widget w1;
Widget w2(w1); // 복사 생성

Widget w3;
w3 = w1;       // 복사 대입&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이게 문제가 없는 클래스라면 괜찮지만, 모든 클래스가 복사되어도 안전한 것은 아니다.&lt;/p&gt;
&lt;h3&gt;2-2. 멤버 단위 복사는 생각보다 단순하다&lt;/h3&gt;
&lt;p&gt;컴파일러가 자동으로 만드는 복사 동작은 대체로 &lt;strong&gt;멤버별 복사(memberwise copy)&lt;/strong&gt; 다.&lt;br&gt;즉 객체 전체 의미를 깊게 이해하고 복사하는 것이 아니라, 각 멤버를 그냥 복사하는 쪽에 가깝다.&lt;/p&gt;
&lt;p&gt;예를 들어 포인터가 멤버로 들어 있으면, 포인터가 가리키는 실제 대상까지 새로 복제해 주는 것이 아니라 주소값만 복사하는 식으로 동작할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Person
{
private:
    char* name;

public:
    Person(char* name) : name(name) {}
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이런 객체를 그냥 복사하면, 두 객체가 같은 &lt;code&gt;name&lt;/code&gt; 메모리를 같이 가리키게 될 수 있다.&lt;br&gt;그러면 누가 메모리를 해제해야 하는지부터 꼬이기 시작한다.&lt;/p&gt;
&lt;p&gt;다시 말해서, 자동 생성 함수는 편리하지만, &lt;strong&gt;클래스 의미에 맞지 않는 복사까지 허용할 수 있다는 점이 위험&lt;/strong&gt;하다.&lt;/p&gt;
&lt;h2&gt;3. &amp;quot;복사되면 안 되는 클래스&amp;quot;는 실제로 많다&lt;/h2&gt;
&lt;h3&gt;3-1. 예: 잠금 객체&lt;/h3&gt;
&lt;p&gt;뮤텍스 같은 잠금 객체를 감싸는 클래스를 생각해 보자.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Lock
{
private:
    Mutex* mutexPtr;

public:
    explicit Lock(Mutex* pm) : mutexPtr(pm)
    {
        lock(mutexPtr);
    }

    ~Lock()
    {
        unlock(mutexPtr);
    }
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 클래스의 의미는 &amp;quot;생성될 때 잠그고, 소멸될 때 푼다&amp;quot;에 가깝다. 그런데 이 객체가 복사되면 어떤 일이 벌어질까?&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;Lock l1(&amp;amp;m);
Lock l2(l1);   // 이 복사가 과연 말이 될까?&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;문제가 바로 보인다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;l1&lt;/code&gt;과 &lt;code&gt;l2&lt;/code&gt;가 같은 뮤텍스를 관리하게 될 수 있다.&lt;/li&gt;
&lt;li&gt;둘 다 소멸될 때 &lt;code&gt;unlock&lt;/code&gt;을 호출하려 들 수 있다.&lt;/li&gt;
&lt;li&gt;잠금의 소유권이 누구에게 있는지 애매해진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉 이 클래스는 &amp;quot;복사 가능&amp;quot;보다는 &lt;strong&gt;&amp;quot;복사 금지&amp;quot;&lt;/strong&gt; 가 맞다.&lt;/p&gt;
&lt;h3&gt;3-2. 예: 유일해야 하는 객체&lt;/h3&gt;
&lt;p&gt;어떤 객체는 설계 자체가 &amp;quot;하나의 실체&amp;quot;를 표현한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;설정 관리자&lt;/li&gt;
&lt;li&gt;로그 관리자&lt;/li&gt;
&lt;li&gt;하드웨어 장치 핸들&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이런 대상을 무심코 복사 가능하게 두면, 코드 사용하는 쪽에서는 &amp;quot;그냥 값처럼 복사해도 되나 보다&amp;quot;라고 오해하기 쉽다.&lt;/p&gt;
&lt;p&gt;그래서 설계 의도를 복사가 가능하면 의미를 명확하게 제공하고, 불가능하면 아예 컴파일이 불가하도록 코드에 박아 넣어야 한다.&lt;/p&gt;
&lt;h2&gt;4. 해결 방법: 복사 관련 함수를 private으로 선언만 하자&lt;/h2&gt;
&lt;p&gt;책에서 제안하는 전통적인 방법은 아래와 같다.&lt;/p&gt;
&lt;h3&gt;4-1. 복사 생성자와 복사 대입 연산자를 private에 넣는다&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class HomeForSale
{
private:
    HomeForSale(const HomeForSale&amp;amp;);
    HomeForSale&amp;amp; operator=(const HomeForSale&amp;amp;);

public:
    HomeForSale() {}
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 코드는 두 가지를 노린다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;클래스 바깥에서 복사하려고 하면 접근이 안 된다.&lt;/li&gt;
&lt;li&gt;정의를 제공하지 않으므로, 혹시 내부에서 잘못 써도 링크 단계에서 걸릴 수 있다.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;4-2. 왜 선언만 하고 정의는 안 하는가&lt;/h3&gt;
&lt;p&gt;복사 금지가 목적이라면, 이 함수들이 호출될 일이 없어야 한다. 그래서 일부러 &lt;strong&gt;선언만 하고 정의는 만들지 않는다.&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class HomeForSale
{
private:
    HomeForSale(const HomeForSale&amp;amp;);            // 선언만
    HomeForSale&amp;amp; operator=(const HomeForSale&amp;amp;); // 선언만
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이렇게 해 두면:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;외부 코드가 복사 시도 → &lt;code&gt;private&lt;/code&gt; 접근 에러&lt;/li&gt;
&lt;li&gt;멤버 함수나 friend 함수가 실수로 사용 → 정의가 없어 링크 에러 가능&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;friend 함수가 낯설기도 하고 방식도 그닥 예뻐보이진 않지만, C++11 이전에는 매우 널리 쓰이던 방법이라고 한다.&lt;/p&gt;
&lt;h2&gt;5. 왜 public에 두고 &amp;quot;호출하지 마세요&amp;quot;라고 하면 안 되는가&lt;/h2&gt;
&lt;p&gt;가끔은 주석이나 문서만으로 복사를 막으려는 코드가 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class HomeForSale
{
public:
    HomeForSale() {}
    // 복사하지 마세요!
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 방식은 거의 의미가 없다. 컴파일러는 주석을 읽지 않기 때문이다.&lt;/p&gt;
&lt;p&gt;실제 개발에서는 아래 같은 일이 왕왕 생긴다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;다른 사람이 클래스 의도를 모른 채 복사한다.&lt;/li&gt;
&lt;li&gt;시간이 지나서 작성자 본인도 설계를 잊는다.&lt;/li&gt;
&lt;li&gt;컨테이너에 넣거나 함수 인자로 넘기다가 복사가 발생한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;그래서 문서로만 막지 말고, 문법 차원에서 아예 방어해야 한다.&lt;/p&gt;
&lt;h2&gt;6. base class로 복사 금지 기능을 재사용할 수 있다&lt;/h2&gt;
&lt;p&gt;복사 금지 클래스가 많아지면, 매번 같은 코드를 반복하게 된다. 이럴 때는 복사 금지용 기반 클래스를 하나 만들어서 재사용할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Uncopyable
{
private:
    Uncopyable(const Uncopyable&amp;amp;);
    Uncopyable&amp;amp; operator=(const Uncopyable&amp;amp;);

protected:
    Uncopyable() {}
    ~Uncopyable() {}
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이제 복사를 막고 싶은 클래스는 이 클래스를 상속받으면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class HomeForSale : private Uncopyable
{
public:
    HomeForSale() {}
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 방식의 장점은 의도가 눈에 바로 들어오고, 중복 코드가 줄어들어서 여러 클래스에 같은 정책을 쉽게 적용할 수 있다는 점이다.&lt;/p&gt;
&lt;h3&gt;6-1. 생성자와 소멸자가 protected인 이유&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;Uncopyable&lt;/code&gt;은 직접 객체를 만들기 위한 클래스가 아니라, 다른 클래스의 기반 클래스로만 쓰기 위한 도구다. 그래서 생성자와 소멸자를 &lt;code&gt;protected&lt;/code&gt;에 둔다.&lt;/p&gt;
&lt;h2&gt;7. 이 방법이 실제로 막는 것은 무엇인가&lt;/h2&gt;
&lt;p&gt;이 패턴은 주로 아래 두 동작을 막는다.&lt;/p&gt;
&lt;h3&gt;7-1. 복사 생성&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;HomeForSale h1;
HomeForSale h2(h1); // 에러&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;7-2. 복사 대입&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;HomeForSale h1;
HomeForSale h2;

h2 = h1; // 에러&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;즉 &amp;quot;새 객체를 기존 객체로부터 만드는 것&amp;quot;과 &amp;quot;이미 있는 객체에 다른 객체 값을 대입하는 것&amp;quot; 둘 다 금지된다.&lt;/p&gt;
&lt;p&gt;한쪽만 막고 다른 쪽을 열어 두면 여전히 복사 의미가 새어 나갈 수 있다. 그래서 보통은 둘을 같이 막는다.&lt;/p&gt;
&lt;h2&gt;8. 핵심은 &amp;quot;설계 의도를 타입에 반영하라&amp;quot;는 것이다&lt;/h2&gt;
&lt;p&gt;내 코드도 그렇지만, 많은 초보 코드에서는 클래스 설계와 실제 문법이 따로 논다.&lt;/p&gt;
&lt;p&gt;예를 들면:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;개념적으로는 복사되면 안 되는 객체인데&lt;/li&gt;
&lt;li&gt;문법상으로는 멀쩡히 복사 가능하다&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 상태가 제일 위험하다.&lt;br&gt;사용하는 쪽에서 틀린 방식으로 써도, 컴파일러가 막아주지 않기 때문이다.&lt;/p&gt;
&lt;p&gt;좋은 클래스 설계는 복사 가능 여부에 따라 아래처럼 분기별로 방어를 잘 해두어야 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;복사 가능한 타입이면: 복사 동작을 자연스럽고 안전하게 제공한다.&lt;/li&gt;
&lt;li&gt;복사 불가능한 타입이면: 애초에 복사 문법이 막혀 있어야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;9. C++11 이후에는 &lt;code&gt;= delete&lt;/code&gt;가 더 직접적이다&lt;/h2&gt;
&lt;p&gt;이번 6챕터는 전통적인 기법을 설명하지만, 현대 C++에서는 더 명확한 방법이 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class HomeForSale
{
public:
    HomeForSale() = default;
    HomeForSale(const HomeForSale&amp;amp;) = delete;
    HomeForSale&amp;amp; operator=(const HomeForSale&amp;amp;) = delete;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 방식의 장점:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;의도가 훨씬 바로 보인다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;private&lt;/code&gt; + 선언만 하는 우회 기법보다 읽기 쉽다.&lt;/li&gt;
&lt;li&gt;컴파일 에러 메시지도 보통 더 직접적이다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉 요즘 C++에서는 보통 &lt;code&gt;= delete&lt;/code&gt;가 더 좋은 선택이다. 다만 Effective C++가 쓰이던 시기의 배경을 이해하려면, &lt;code&gt;private&lt;/code&gt; 선언 기법도 알아둘 가치가 있다.&lt;/p&gt;
&lt;h3&gt;9-1. 그래도 옛 기법을 알아야 하는 이유&lt;/h3&gt;
&lt;p&gt;실무에서는 오래된 코드베이스를 자주 만난다. 그 안에는 아직도 이런 형태가 남아 있을 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Uncopyable
{
private:
    Uncopyable(const Uncopyable&amp;amp;);
    Uncopyable&amp;amp; operator=(const Uncopyable&amp;amp;);
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이걸 봤을 때 &amp;quot;왜 굳이 이렇게 복잡하게 썼지?&amp;quot;가 아니라, &amp;quot;아, 복사 금지 의도를 표현한 오래된 패턴이구나&amp;quot;를 바로 읽어낼 수 있어야 하겠다.&lt;/p&gt;
&lt;h2&gt;10. 실무에서 어떻게 판단하면 좋은가&lt;/h2&gt;
&lt;p&gt;클래스를 만들 때 한 번은 꼭 자문해 보는 편이 좋다.&lt;/p&gt;
&lt;h3&gt;10-1. 이 객체는 복사되어도 자연스러운가&lt;/h3&gt;
&lt;p&gt;예를 들어 &lt;code&gt;Point&lt;/code&gt;, &lt;code&gt;std::string&lt;/code&gt; 같은 값 타입은 복사가 자연스럽다. 복사본이 생겨도 의미가 크게 꼬이지 않는다.&lt;/p&gt;
&lt;h3&gt;10-2. 이 객체는 자원 소유권을 가지는가&lt;/h3&gt;
&lt;p&gt;파일, 소켓, 락, 데이터베이스 연결처럼 외부 자원을 직접 관리하면 복사 의미가 민감해진다.&lt;/p&gt;
&lt;p&gt;반면 아래와 같은 경우는 특히 신중해야 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;진짜 복사가 필요하다면 복사 규칙을 직접 설계한다&lt;/li&gt;
&lt;li&gt;아니면 복사를 금지한다&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;11. 항목 6 핵심 정리&lt;/h2&gt;
&lt;h3&gt;반드시 기억할 것&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;컴파일러가 자동으로 만드는 복사 관련 함수가 항상 올바른 것은 아니다.&lt;/li&gt;
&lt;li&gt;어떤 클래스는 설계상 복사되면 안 되므로, 복사 자체를 금지해야 한다.&lt;/li&gt;
&lt;li&gt;Effective C++의 전통적인 방법은 복사 생성자와 복사 대입 연산자를 &lt;code&gt;private&lt;/code&gt;에 선언만 해 두는 것이다.&lt;/li&gt;
&lt;li&gt;같은 정책이 반복되면 &lt;code&gt;Uncopyable&lt;/code&gt; 같은 기반 클래스로 재사용할 수 있다.&lt;/li&gt;
&lt;li&gt;현대 C++에서는 보통 &lt;code&gt;= delete&lt;/code&gt;가 더 명확하고 읽기 좋다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>책/Effective C++</category>
      <author>Sonji</author>
      <guid isPermaLink="true">https://sonlog.tistory.com/16</guid>
      <comments>https://sonlog.tistory.com/entry/Effective-C-Cp-6-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC%EA%B0%80-%EB%A7%8C%EB%93%A4%EC%96%B4%EC%A3%BC%EB%8A%94-%ED%95%A8%EC%88%98%EB%A5%BC-%EC%9B%90%EC%B9%98-%EC%95%8A%EC%9C%BC%EB%A9%B4-%EC%82%AC%EC%9A%A9-%EA%B8%88%EC%A7%80%EC%8B%9C%ED%82%A4%EC%9E%90#entry16comment</comments>
      <pubDate>Mon, 13 Apr 2026 17:25:07 +0900</pubDate>
    </item>
    <item>
      <title>[Effective C++] Cp 5. 컴파일러가 만든 함수도 다시보자</title>
      <link>https://sonlog.tistory.com/entry/Effective-C-Cp-5-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC%EA%B0%80-%EB%A7%8C%EB%93%A0-%ED%95%A8%EC%88%98%EB%8F%84-%EB%8B%A4%EC%8B%9C%EB%B3%B4%EC%9E%90</link>
      <description>&lt;p&gt;이 항목의 핵심은 간단하다.&lt;/p&gt;
&lt;p&gt;클래스의 생성/복사/대입/소멸 동작을 내가 명시하지 않으면 컴파일러가 &amp;quot;합리적이라고 판단한 기본 동작&amp;quot;을 넣어 준다.&lt;br&gt;문제는 이 기본 동작이 &amp;quot;컴파일은 되지만 의도는 틀린 코드&amp;quot;를 만들 수 있다는 점이다. 특히 리소스 소유권(메모리, 파일, 소켓, 뮤텍스)을 다루는 클래스에서 위험하다.&lt;/p&gt;
&lt;h3&gt;1. 컴파일러가 자동으로 만들 수 있는 함수&lt;/h3&gt;
&lt;p&gt;C++ 클래스에서 특별히 선언하지 않으면 컴파일러가 다음 멤버 함수를 자동으로 선언할 수 있다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;기본 생성자(default constructor)&lt;/li&gt;
&lt;li&gt;소멸자(destructor)&lt;/li&gt;
&lt;li&gt;복사 생성자(copy constructor)&lt;/li&gt;
&lt;li&gt;복사 대입 연산자(copy assignment operator)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;C++11 이후에는 이동 의미(move semantics)가 추가되어 아래도 상황에 따라 자동 생성된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;이동 생성자(move constructor)&lt;/li&gt;
&lt;li&gt;이동 대입 연산자(move assignment operator)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;주의할 점은 &amp;quot;항상 자동 생성&amp;quot;이 아니라 &amp;quot;조건이 맞을 때 자동 생성&amp;quot;이라는 점이다. 일부 함수를 직접 선언하면 다른 함수의 자동 생성이 억제되거나 삭제(&lt;code&gt;= delete&lt;/code&gt;)될 수 있다.&lt;/p&gt;
&lt;h3&gt;2. 자동 생성 동작이 왜 문제를 만들까&lt;/h3&gt;
&lt;p&gt;자동 생성된 복사/대입은 기본적으로 &amp;quot;멤버별 복사(memberwise copy)&amp;quot;다.&lt;br&gt;멤버가 값 타입(&lt;code&gt;int&lt;/code&gt;, &lt;code&gt;std::string&lt;/code&gt;, &lt;code&gt;std::vector&lt;/code&gt;)이면 보통 문제 없다.하지만 &amp;quot;소유권 있는 포인터&amp;quot;가 멤버면 얕은 복사가 일어나기 쉽다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class FileHandle
{
private:
    FILE* fp;

public:
    explicit FileHandle(FILE* f)
        : fp(f)
    { }

    ~FileHandle()
    {
        if (fp != nullptr)
        {
            fclose(fp);
        }
    }
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 코드에서 복사 생성자/복사 대입을 직접 정의하지 않으면 &lt;code&gt;fp&lt;/code&gt; 주소값만 복사된 객체가 생길 수 있다. 그 결과 다음 문제가 생긴다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;두 객체가 같은 파일 핸들을 닫으면서 이중 해제 발생&lt;/li&gt;
&lt;li&gt;한 객체가 먼저 닫아 버린 뒤 다른 객체가 잘못된 핸들 접근&lt;/li&gt;
&lt;li&gt;소멸 순서에 따라 간헐적 버그(재현 어려움)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;3. &amp;quot;자동 생성/삭제&amp;quot;가 갈리는 대표 조건&lt;/h3&gt;
&lt;p&gt;이하는 개발 중 자주 걸리는 규칙들이다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;참조 멤버(&lt;code&gt;T&amp;amp;&lt;/code&gt;)가 있으면 복사 대입이 자연스럽게 어렵다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;const&lt;/code&gt; 멤버가 있으면 복사 대입 시 재할당이 불가능해 제약이 생긴다.&lt;/li&gt;
&lt;li&gt;사용자 정의 소멸자/복사 연산자 선언은 이동 연산 자동 생성을 막을 수 있다.&lt;/li&gt;
&lt;li&gt;멤버/기반 클래스가 복사 불가면 해당 클래스 복사도 불가가 된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;즉, &lt;strong&gt;나는 하나만 건드렸는데 다른 연산이 갑자기 delete되는&lt;/strong&gt; 현상이 실제로 발생하므로, 클래스 인터페이스만 보고도 복사/이동 가능 여부를 명시적으로 확인해야 한다.&lt;/p&gt;
&lt;h3&gt;4. Rule of Three / Five 관점에서 정리&lt;/h3&gt;
&lt;p&gt;고전적으로는 Rule of Three가 중요하다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;소멸자&lt;/li&gt;
&lt;li&gt;복사 생성자&lt;/li&gt;
&lt;li&gt;복사 대입 연산자&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 셋 중 하나를 직접 작성해야 하는 클래스라면, 대개 나머지 둘도 함께 설계해야 한다는 뜻이다.&lt;/p&gt;
&lt;p&gt;C++11 이후에는 이동 연산까지 포함해 Rule of Five로 확장한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;소멸자&lt;/li&gt;
&lt;li&gt;복사 생성자&lt;/li&gt;
&lt;li&gt;복사 대입 연산자&lt;/li&gt;
&lt;li&gt;이동 생성자&lt;/li&gt;
&lt;li&gt;이동 대입 연산자&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;5. 권장 대응 방식&lt;/h3&gt;
&lt;p&gt;가장 안전한 우선순위는 다음과 같다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;리소스를 직접 다루지 말고 RAII 표준 타입을 먼저 사용&lt;/li&gt;
&lt;li&gt;복사가 불필요한 타입은 명시적으로 복사 금지&lt;/li&gt;
&lt;li&gt;복사가 필요하면 깊은 복사 정책을 코드로 분명히 구현&lt;/li&gt;
&lt;li&gt;이동만 허용할지, 복사+이동 모두 허용할지 의도 명시&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;복사 금지 예시는 아래처럼 표현한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Uncopyable
{
private:
    Uncopyable(const Uncopyable&amp;amp;) = delete;
    Uncopyable&amp;amp; operator=(const Uncopyable&amp;amp;) = delete;

public:
    Uncopyable() = default;
    ~Uncopyable() = default;
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;6. 깊은 복사가 필요한 경우의 형태&lt;/h3&gt;
&lt;p&gt;소유 포인터를 반드시 써야 한다면 복사 생성자/복사 대입에서 &amp;quot;새 자원 생성 + 내용 복사&amp;quot;를 직접 정의해야 한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Buffer
{
private:
    std::size_t size;
    int* data;

public:
    explicit Buffer(std::size_t n)
        : size(n), data(new int[n]())
    { }

    ~Buffer()
    {
        delete[] data;
    }

    Buffer(const Buffer&amp;amp; rhs)
        : size(rhs.size), data(new int[rhs.size])
    {
        std::copy(rhs.data, rhs.data + size, data);
    }

    Buffer&amp;amp; operator=(const Buffer&amp;amp; rhs)
    {
        if (this == &amp;amp;rhs)
        {
            return *this;
        }

        int* newData = new int[rhs.size];
        std::copy(rhs.data, rhs.data + rhs.size, newData);

        delete[] data;
        data = newData;
        size = rhs.size;

        return *this;
    }
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이런 코드가 반복된다면 원시 포인터 대신 &lt;code&gt;std::vector&amp;lt;int&amp;gt;&lt;/code&gt;를 쓰는 편이 훨씬 안전하다.&lt;/p&gt;
&lt;h3&gt;7. 정리&lt;/h3&gt;
&lt;p&gt;컴파일러의 자동 생성 함수는 편의 기능이지 설계 의도를 대신해 주는 기능이 아니라는 점을 잊지말자.&lt;br&gt;클래스가 무엇을 소유하고, 복사/이동/소멸을 어떻게 해야 하는지 직접 결정하고, 그 결정을 &lt;code&gt;= default&lt;/code&gt;/&lt;code&gt;= delete&lt;/code&gt;/사용자 정의 함수로 명시하는 습관이 필요하다.&lt;/p&gt;</description>
      <category>책/Effective C++</category>
      <author>Sonji</author>
      <guid isPermaLink="true">https://sonlog.tistory.com/15</guid>
      <comments>https://sonlog.tistory.com/entry/Effective-C-Cp-5-%EC%BB%B4%ED%8C%8C%EC%9D%BC%EB%9F%AC%EA%B0%80-%EB%A7%8C%EB%93%A0-%ED%95%A8%EC%88%98%EB%8F%84-%EB%8B%A4%EC%8B%9C%EB%B3%B4%EC%9E%90#entry15comment</comments>
      <pubDate>Mon, 13 Apr 2026 02:47:53 +0900</pubDate>
    </item>
    <item>
      <title>[Effective C++] Cp 4. 객체를 사용하기 전 반드시 초기화</title>
      <link>https://sonlog.tistory.com/entry/Effective-C-Cp-4-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-%EC%A0%84-%EB%B0%98%EB%93%9C%EC%8B%9C-%EC%B4%88%EA%B8%B0%ED%99%94</link>
      <description>&lt;h2 data-ke-size=&quot;size26&quot;&gt;1. 왜 초기화가 중요한가&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;초기화되지 않은 객체를 쓰면 버그가 바로 드러날 때도 있고, 꽤 잘 재현되는 것처럼 보일 때도 있다.&lt;br /&gt;문제는 그 재현성이 신뢰할 만하지 않다는 점이다. 빌드 옵션이나 코드 배치, 실행 환경이 바뀌면 증상이 달라지거나 갑자기 사라질 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;핵심은 단순하다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;초기화되지 않은 값은 믿을 수 없다.&lt;/b&gt;&lt;/li&gt;
&lt;li&gt;&lt;b&gt;컴파일러가 항상 자동으로 올바른 초기화를 해주지 않는다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;특히 기본 타입(&lt;code&gt;int&lt;/code&gt;, &lt;code&gt;double&lt;/code&gt;, 포인터 등)은 상황에 따라 초기화가 되기도 하고 안 되기도 한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;int x;          // 초기화되지 않음 (값 미정)
double y;       // 초기화되지 않음 (값 미정)
int* p;         // 초기화되지 않음 (쓰레기 주소 가능)&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 상태에서 값을 읽으면 동작이 예측 불가능해진다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;2. &quot;자동으로 초기화될 것&quot;이라는 기대를 버리자&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-1. 기본 타입과 사용자 정의 타입의 차이&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;사용자 정의 타입(클래스)은 생성자가 호출되면서 대체로 초기화가 진행된다.&lt;br /&gt;심지어 아예 생성자를 명시적으로 적지 않더라도, 컴파일러가 디폴트 생성자를 삽입해서 런타임에 초기화되도록 최적화를 지원하기도 한다.&lt;br /&gt;반면 기본 타입은 직접 값을 주지 않으면 그대로 남는다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class Point
{
private:
    int x;
    int y;

public:
    Point() : x(0), y(0) {}
};

Point pt;   // 생성자 호출로 초기화
int n;      // 초기화되지 않음&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;그래서 &quot;클래스니까 괜찮겠지&quot;, &quot;지역 변수니까 0이겠지&quot; 같은 가정은 매우 위험하다.&lt;/p&gt;
&lt;p&gt;&lt;del&gt;사실 이런 가정 자체가 말이 안된다. 해서도 안된다.&lt;/del&gt;&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;2-2. 안전한 기본 습관&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;값이 필요한 변수는 선언과 동시에 초기화한다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;int n = 0;
double ratio = 0.0;
int* p = nullptr;&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 습관 하나로 디버깅 시간을 크게 줄일 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;3. 생성자 본문 대입보다 초기화 리스트를 우선하자&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-1. 본문 대입은 &quot;초기화&quot;가 아니라 &quot;대입&quot;이다&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아래 코드는 얼핏 정상처럼 보이지만, 사실 &quot;초기화&quot;가 아니라 &quot;기본 생성 후 대입&quot;이다.&lt;/p&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;class PhoneNumber
{
public:
    PhoneNumber() {}
};

class ABEntry
{
private:
    std::string theName;
    std::string theAddress;
    std::list&amp;lt;PhoneNumber&amp;gt; thePhones;
    int numTimesConsulted;

public:
    ABEntry(const std::string&amp;amp; name,
            const std::string&amp;amp; address,
            const std::list&amp;lt;PhoneNumber&amp;gt;&amp;amp; phones)
    {
        this-&amp;gt;theName = name;        // 이미 기본 생성된 뒤 대입
        this-&amp;gt;theAddress = address;  // 이미 기본 생성된 뒤 대입
        this-&amp;gt;thePhones = phones;    // 이미 기본 생성된 뒤 대입
        this-&amp;gt;numTimesConsulted = 0;
    }
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;멤버들은 생성자 본문이 실행되기 전에 먼저 생성된다.&lt;br /&gt;즉 위 코드는 &quot;기본 생성 + 대입&quot; 2단계를 거치므로 불필요한 작업이 생길 수 있다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-2. 초기화 리스트가 정석&lt;/h3&gt;
&lt;pre class=&quot;cpp&quot;&gt;&lt;code&gt;class ABEntry
{
private:
    std::string theName;
    std::string theAddress;
    std::list&amp;lt;PhoneNumber&amp;gt; thePhones;
    int numTimesConsulted;

public:
    ABEntry(const std::string&amp;amp; name,
            const std::string&amp;amp; address,
            const std::list&amp;lt;PhoneNumber&amp;gt;&amp;amp; phones)
        : theName(name),
          theAddress(address),
          thePhones(phones),
          numTimesConsulted(0)
    {}
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 방식은 멤버를 &quot;처음부터 원하는 값으로&quot; 만들기 때문에 더 자연스럽고, 대체로 더 효율적이다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;3-3. const 멤버와 reference 멤버는 초기화 리스트가 필수&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;const&lt;/code&gt;와 참조(&lt;code&gt;&amp;amp;&lt;/code&gt;) 멤버는 생성 후 재대입이 불가능하다.&lt;br /&gt;그래서 생성자 본문 대입 방식은 아예 동작하지 않는다.&lt;/p&gt;
&lt;pre class=&quot;css&quot;&gt;&lt;code&gt;class Widget
{
private:
    const int id;
    std::string&amp;amp; name;

public:
    Widget(int id, std::string&amp;amp; name)
        : id(id), name(name)  // 필수
    {}
};&lt;/code&gt;&lt;/pre&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;4. 멤버 초기화 순서는 &quot;리스트 순서&quot;가 아니라 &quot;선언 순서&quot;&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;많이 헷갈리는 부분이다.&lt;br /&gt;초기화는 생성자 초기화 리스트에 적은 순서가 아니라, &lt;b&gt;클래스에 선언된 순서&lt;/b&gt;로 실행된다.&lt;/p&gt;
&lt;pre class=&quot;angelscript&quot;&gt;&lt;code&gt;class Example
{
private:
    int a;
    int b;

public:
    Example()
        : b(2), a(b) // 이렇게 써도 실제 초기화는 a -&amp;gt; b 순서
    {}
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 코드에서 &lt;code&gt;a(b)&lt;/code&gt;는 의도와 다르게 동작할 수 있다. &lt;code&gt;a&lt;/code&gt;가 먼저 초기화되기 때문이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;안전한 규칙은 아래 한 줄로 정리된다.&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;b&gt;멤버 선언 순서와 초기화 리스트 순서를 동일하게 맞춘다.&lt;/b&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;코드 리뷰에서도 이 규칙을 강하게 지키면 초기화 관련 버그를 많이 줄일 수 있다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;5. 비지역 정적 객체의 초기화 순서 문제&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-1. 문제의 핵심&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;서로 다른 소스 파일(번역 단위)에 있는 비지역 정적 객체(non-local static object)들은&lt;br /&gt;초기화 순서가 보장되지 않는다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;예를 들면:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;&lt;code&gt;FileA.cpp&lt;/code&gt;의 정적 객체 &lt;code&gt;A&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;FileB.cpp&lt;/code&gt;의 정적 객체 &lt;code&gt;B&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;code&gt;A&lt;/code&gt; 생성자에서 &lt;code&gt;B&lt;/code&gt;를 사용하는데, 실행 시점에 &lt;code&gt;B&lt;/code&gt;가 아직 초기화되지 않았을 수 있다.&lt;br /&gt;이게 유명한 &lt;b&gt;정적 초기화 순서 문제(static initialization order fiasco)&lt;/b&gt; 다.&lt;/p&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;5-2. 왜 위험한가&lt;/h3&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 버그는 빌드 옵션, 링크 순서, 플랫폼에 따라 재현 여부가 달라진다.&lt;br /&gt;즉 &quot;내 PC에서는 잘 됨&quot;이 가장 쉽게 나오는 유형이다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;6. 해결책: 함수 지역 정적 객체(Construct on First Use)&lt;/h2&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가장 널리 쓰는 해결책은 &quot;전역처럼 쓰고 싶은 객체를 함수 안 &lt;code&gt;static&lt;/code&gt; 지역 변수로 만들고, 그 함수를 통해서만 접근하는 방식&quot;이다.&lt;br /&gt;필요할 때 처음 생성되도록 만들어 초기화 순서 의존을 줄이는 방식이다.&lt;br /&gt;즉 &quot;프로그램 시작 시점에 무조건 생성&quot;이 아니라, &quot;처음 필요해지는 순간 생성&quot;되게 바꾸는 방식이다.&lt;/p&gt;
&lt;pre class=&quot;arduino&quot;&gt;&lt;code&gt;class FileSystem
{
public:
    std::size_t numDisks() const;
};

FileSystem&amp;amp; tfs()
{
    static FileSystem fs; // 최초 호출 시 1회 초기화
    return fs;
}

class Directory
{
public:
    Directory()
    {
        std::size_t disks = tfs().numDisks();
    }
};&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 패턴의 장점:&lt;/p&gt;
&lt;ul style=&quot;list-style-type: disc;&quot; data-ke-list-type=&quot;disc&quot;&gt;
&lt;li&gt;객체가 실제로 필요할 때 초기화된다.&lt;/li&gt;
&lt;li&gt;번역 단위 간 정적 초기화 순서 의존을 피하기 쉽다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;전역 상태가 필요하다면, 우선 이 패턴으로 시작하는 편이 안전하다.&lt;/p&gt;
&lt;hr data-ke-style=&quot;style1&quot; /&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;7. 핵심 정리&lt;/h2&gt;
&lt;h3 data-ke-size=&quot;size23&quot;&gt;반드시 기억할 것&lt;/h3&gt;
&lt;ol style=&quot;list-style-type: decimal;&quot; data-ke-list-type=&quot;decimal&quot;&gt;
&lt;li&gt;객체는 사용 전에 항상 초기화한다.&lt;/li&gt;
&lt;li&gt;생성자에서는 멤버 대입보다 초기화 리스트를 우선한다.&lt;/li&gt;
&lt;li&gt;멤버 초기화 순서는 선언 순서로 결정된다.&lt;/li&gt;
&lt;li&gt;번역 단위가 다른 비지역 정적 객체끼리의 초기화 순서는 믿지 않는다.&lt;/li&gt;
&lt;li&gt;전역 정적 의존은 함수 지역 정적(Construct on First Use)으로 완화한다.&lt;/li&gt;
&lt;/ol&gt;</description>
      <category>책/Effective C++</category>
      <author>Sonji</author>
      <guid isPermaLink="true">https://sonlog.tistory.com/14</guid>
      <comments>https://sonlog.tistory.com/entry/Effective-C-Cp-4-%EA%B0%9D%EC%B2%B4%EB%A5%BC-%EC%82%AC%EC%9A%A9%ED%95%98%EA%B8%B0-%EC%A0%84-%EB%B0%98%EB%93%9C%EC%8B%9C-%EC%B4%88%EA%B8%B0%ED%99%94#entry14comment</comments>
      <pubDate>Thu, 9 Apr 2026 14:53:36 +0900</pubDate>
    </item>
    <item>
      <title>[OS] System call</title>
      <link>https://sonlog.tistory.com/entry/OS-System-call</link>
      <description>&lt;h1&gt;System Call&lt;/h1&gt;
&lt;h1&gt;목차&lt;/h1&gt;
&lt;ul&gt;
&lt;li&gt;시스템 콜 정의&lt;/li&gt;
&lt;li&gt;시스템 콜 제공&lt;/li&gt;
&lt;li&gt;시스템 콜 세부구성&lt;/li&gt;
&lt;li&gt;시스템 콜 구현&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;시스템 콜 정의&lt;/h2&gt;
&lt;p&gt;시스템 콜(System Call)은 &lt;strong&gt;사용자 공간(User Space)&lt;/strong&gt; 과 &lt;strong&gt;커널 공간(Kernel Space)&lt;/strong&gt; 사이를 이어주는 인터페이스다.&lt;/p&gt;
&lt;p&gt;시스템 콜이 필요한 이유는 크게 3가지다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;하드웨어를 직접 다루지 않아도 되게 해준다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;예) 파일 입출력을 할 때, 애플리케이션은 디스크 구조나 파일시스템 내부 구현을 몰라도 된다.&lt;/p&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;보안과 안정성을 지켜준다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;예) 커널이 중간에서 권한 검사와 자원 접근 통제를 수행한다.&lt;/p&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;프로세스마다 가상화된 실행 환경을 제공한다.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;예) 각 프로세스는 자신만의 주소 공간을 가진 것처럼 동작한다.&lt;/p&gt;
&lt;p&gt;리눅스에서 사용자 공간 프로세스가 커널 기능을 사용하려면, 기본적으로 시스템 콜 경로를 거쳐야 한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;트랩(Trap): 소프트웨어가 발생시키는 동기식 인터럽트.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;예) 0으로 나누기, 페이지 폴트.&lt;/p&gt;
&lt;p&gt;x86 기준 시스템 콜은 수백 개(대략 300개 이상)가 있으며, 아키텍처별로 일부 차이가 있다.&lt;/p&gt;
&lt;h2&gt;syscall&lt;/h2&gt;
&lt;p&gt;리눅스에서는 system call을 줄여서 &lt;code&gt;syscall&lt;/code&gt;이라고 부른다. 보통 사용자 코드는 라이브러리 함수를 호출하고, 내부에서 시스템 콜이 실행된다.&lt;/p&gt;
&lt;p&gt;예) 파일에 데이터 쓰기, 프로세스 ID 조회.&lt;/p&gt;
&lt;p&gt;시스템 콜의 반환형은 &lt;code&gt;long&lt;/code&gt;인 경우가 많다. 이는 32비트/64비트 환경 호환성을 고려한 설계다.&lt;/p&gt;
&lt;p&gt;예시로 &lt;code&gt;getpid()&lt;/code&gt;는 현재 프로세스의 PID를 반환한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;SYSCALL_DEFINE0(getpid)
{
    return task_tgid_vnr(current);
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;각 요소의 의미:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;SYSCALL_DEFINE0&lt;/code&gt;: 인자 0개인 시스템 콜 정의 매크로&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getpid&lt;/code&gt;: 시스템 콜 이름&lt;/li&gt;
&lt;li&gt;&lt;code&gt;task_tgid_vnr(current)&lt;/code&gt;: 현재 태스크의 TGID 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;왜 &lt;code&gt;getpid()&lt;/code&gt;에서 TGID를 반환할까?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;일반적인 단일 스레드 프로세스에서는 PID와 TGID가 동일하다.&lt;/li&gt;
&lt;li&gt;그래서 결과적으로 사용자 입장에서는 PID를 얻는 것과 같다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이 매크로는 내부적으로 아래 형태의 심볼로 연결된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;asmlinkage long sys_getpid(void);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;각 요소의 의미:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;asmlinkage&lt;/code&gt;: 인자를 스택에서 전달받도록 컴파일러에 알리는 키워드&lt;/li&gt;
&lt;li&gt;반환형 &lt;code&gt;long&lt;/code&gt;: 아키텍처 호환성 유지&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sys_&lt;/code&gt;: 시스템 콜 구현 함수 접두어&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;예) &lt;code&gt;bar()&lt;/code&gt; 시스템 콜 구현 함수 이름은 &lt;code&gt;sys_bar()&lt;/code&gt; 형태.&lt;/p&gt;
&lt;p&gt;대부분의 시스템 콜은 호출 번호 + 매개변수로 커널에 전달되며, 아키텍처 규약에 따라 레지스터를 사용한다.&lt;/p&gt;
&lt;h2&gt;시스템 콜 제공&lt;/h2&gt;
&lt;p&gt;애플리케이션은 보통 시스템 콜을 직접 호출하지 않고, 사용자 공간 API를 통해 간접적으로 사용한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;API: Application Programming Interface&lt;/li&gt;
&lt;li&gt;대표 표준: POSIX(Portable Operating System Interface)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;POSIX 덕분에 운영체제 내부 구현이 달라도, 애플리케이션은 비슷한 인터페이스를 사용할 수 있다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Sonji-1/CS_study/main/Operating%20System/System_Call/Untitled.png&quot; alt=&quot;시스템 콜 인터페이스 개요&quot;&gt;&lt;/p&gt;
&lt;p&gt;리눅스의 시스템 콜 인터페이스는 보통 C 라이브러리(예: glibc)를 통해 제공된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;개발자는 API 사용에 집중&lt;/li&gt;
&lt;li&gt;커널은 시스템 콜 구현과 안정성에 집중&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;유닉스 철학의 핵심 문장:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Provide mechanism, not policy.&lt;/li&gt;
&lt;li&gt;정책(어떻게 쓸지)은 사용자/프로그램이 결정하고, 커널은 수단(메커니즘)을 제공한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;사용자 공간에서 시스템 콜&lt;/h2&gt;
&lt;p&gt;일반적으로 C 라이브러리가 시스템 콜 진입 과정을 감싼 래퍼(wrapper)를 제공한다.&lt;/p&gt;
&lt;p&gt;즉, 애플리케이션은 표준 헤더와 라이브러리 링크만으로 시스템 콜을 쓸 수 있다.&lt;/p&gt;
&lt;p&gt;리눅스에는 과거 &lt;code&gt;_syscalln()&lt;/code&gt; 계열 매크로가 있었고, 여기서 &lt;code&gt;n&lt;/code&gt;은 인자 개수(0~6)를 의미했다.&lt;/p&gt;
&lt;p&gt;예시:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-c&quot;&gt;long open(const char *filename, int flags, int mode)

#define __NR_open 5
_syscall3(long, open, const char *, filename, int, flags, int, mode)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;핵심 포인트:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;앞 2개 인자: 반환형, 시스템 콜 이름&lt;/li&gt;
&lt;li&gt;그 뒤 인자들: &lt;code&gt;(타입, 이름)&lt;/code&gt; 쌍&lt;/li&gt;
&lt;li&gt;&lt;code&gt;__NR_open&lt;/code&gt;: 시스템 콜 번호&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;알맞은 시스템 콜 찾기&lt;/h2&gt;
&lt;p&gt;사용자 공간에서 커널로 진입해도, &lt;strong&gt;어떤 시스템 콜을 실행할지&lt;/strong&gt; 번호로 알려줘야 한다.&lt;/p&gt;
&lt;p&gt;x86 아키텍처에서는 전통적으로 &lt;code&gt;eax&lt;/code&gt; 레지스터를 사용해 시스템 콜 번호를 전달한다.&lt;/p&gt;
&lt;p&gt;흐름:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;사용자 공간에서 시스템 콜 번호 준비&lt;/li&gt;
&lt;li&gt;예외/트랩을 통해 커널 모드 진입&lt;/li&gt;
&lt;li&gt;시스템 콜 핸들러가 번호를 읽어 해당 구현으로 분기&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;커널은 번호가 유효한지 &lt;code&gt;NR_syscalls&lt;/code&gt; 범위를 기준으로 확인한다.&lt;/p&gt;
&lt;h2&gt;시스템 콜 세부구성&lt;/h2&gt;
&lt;p&gt;시스템 콜은 크게 &lt;strong&gt;번호(System Call Number)&lt;/strong&gt; 와 &lt;strong&gt;핸들러(System Call Handler)&lt;/strong&gt; 를 중심으로 동작한다.&lt;/p&gt;
&lt;h2&gt;시스템 콜 번호&lt;/h2&gt;
&lt;p&gt;시스템 콜 핸들러는 전달받은 번호가 유효한지 먼저 검사한다.&lt;/p&gt;
&lt;p&gt;시스템 콜 번호의 성질:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;한 번 할당된 번호는 바꾸지 않는다.&lt;/li&gt;
&lt;li&gt;제거된 시스템 콜 번호는 보통 재사용하지 않는다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;이유:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;번호를 바꾸거나 재사용하면, 이미 빌드된 기존 프로그램이 잘못된 시스템 콜을 호출할 수 있다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;리눅스는 구현되지 않은 시스템 콜 처리용으로 &lt;code&gt;sys_ni_syscall()&lt;/code&gt;을 제공한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;반환값: &lt;code&gt;-ENOSYS&lt;/code&gt; (해당 시스템 콜 미지원)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;시스템 콜 매핑은 아키텍처별 &lt;code&gt;sys_call_table&lt;/code&gt;에 저장된다.&lt;/p&gt;
&lt;h2&gt;시스템 콜 핸들러&lt;/h2&gt;
&lt;p&gt;사용자 프로세스가 시스템 콜을 실행하려면 커널 모드로 전환되어야 하며, 이를 위해 소프트웨어 인터럽트/예외 메커니즘을 사용한다.&lt;/p&gt;
&lt;p&gt;핸들러의 역할:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;전달받은 시스템 콜 번호 확인&lt;/li&gt;
&lt;li&gt;매개변수 검증&lt;/li&gt;
&lt;li&gt;실제 커널 내부 구현 호출&lt;/li&gt;
&lt;li&gt;결과를 사용자 공간에 반환&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;x86-64에서는 진입 코드가 &lt;code&gt;entry_64.S&lt;/code&gt; 등 아키텍처별 엔트리 코드에 정의되어 있다.&lt;/p&gt;
&lt;h2&gt;시스템 콜 구현&lt;/h2&gt;
&lt;h3&gt;구현 순서&lt;/h3&gt;
&lt;p&gt;시스템 콜을 설계/구현할 때는 아래 순서로 보는 것이 좋다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;목적을 하나로 명확히 정의&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;한 시스템 콜은 한 가지 핵심 목적에 집중하는 것이 유지보수에 유리하다.&lt;/li&gt;
&lt;li&gt;의미와 동작은 공개 후 바꾸기 어렵다(호환성 문제).&lt;/li&gt;
&lt;li&gt;미래 확장을 고려해 플래그 인자를 두는 방식이 자주 사용된다.&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;매개변수 유효성 검증&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;잘못된 파일 디스크립터, 잘못된 PID, 범위 밖 값 등을 검사해야 한다.&lt;/li&gt;
&lt;li&gt;사용자 포인터는 반드시 접근 가능성과 주소 공간 유효성을 확인해야 한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;검증 예시:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;사용자 공간 주소인지 확인&lt;/li&gt;
&lt;li&gt;해당 프로세스 주소 공간인지 확인&lt;/li&gt;
&lt;li&gt;읽기/쓰기 권한이 맞는지 확인&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;3&quot;&gt;
&lt;li&gt;권한 검증&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;capable()&lt;/code&gt;로 필요한 권한을 갖췄는지 검사한다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;예) &lt;code&gt;capable(CAP_SYS_NICE)&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;사용자 공간과 데이터 교환 시 대표 함수:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;copy_to_user(dst, src, size)&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;커널 -&amp;gt; 사용자 공간 복사&lt;/li&gt;
&lt;/ul&gt;
&lt;ol start=&quot;2&quot;&gt;
&lt;li&gt;&lt;code&gt;copy_from_user(dst, src, size)&lt;/code&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;사용자 -&amp;gt; 커널 공간 복사&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;두 함수는 실패 시 남은 바이트 수를 반환하고, 성공 시 0을 반환한다.&lt;/p&gt;
&lt;p&gt;검증/복사 로직까지 완성하면 시스템 콜 테이블에 항목을 추가한다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;지원 아키텍처별 테이블 반영&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unistd.h&lt;/code&gt; 계열 헤더에 번호 정의&lt;/li&gt;
&lt;li&gt;커널 이미지에 포함되도록 빌드&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;시스템 콜 컨택스트&lt;/h2&gt;
&lt;p&gt;시스템 콜 컨텍스트에서는 현재 프로세스가 커널 모드에서 실행된다.&lt;/p&gt;
&lt;p&gt;중요한 특성:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;블로킹(수면) 가능&lt;/li&gt;
&lt;li&gt;선점 가능&lt;/li&gt;
&lt;li&gt;따라서 재진입성을 고려한 동기화/보호가 필요&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;시스템 콜이 끝나면 제어권은 커널의 반환 경로를 거쳐 다시 사용자 공간으로 돌아가고, 사용자 프로세스 실행이 재개된다.&lt;/p&gt;
&lt;h2&gt;추가 조사&lt;/h2&gt;
&lt;h3&gt;소프트웨어 인터럽트&lt;/h3&gt;
&lt;p&gt;인터럽트 섹션에서 별도로 정리 예정.&lt;/p&gt;
&lt;h2&gt;syscall 매크로 정의 위치&lt;/h2&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Sonji-1/CS_study/main/Operating%20System/System_Call/Untitled%201.png&quot; alt=&quot;syscall 매크로 위치&quot;&gt;&lt;/p&gt;
&lt;p&gt;참고자료: &lt;a href=&quot;https://yuldangs-sosolife.tistory.com/entry/kernel%EC%97%90-system-call-%EC%B6%94%EA%B0%80ubuntu&quot;&gt;kernel에 system call 추가(ubuntu)&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;EAX 레지스터&lt;/h3&gt;
&lt;p&gt;정의:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;EAX(Extended Accumulator Register)는 산술/논리 연산과 반환값 전달에 자주 쓰이는 레지스터다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;참고 포인트:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;64비트 레지스터 &lt;code&gt;RAX&lt;/code&gt;의 하위 32비트가 &lt;code&gt;EAX&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;호출 규약/아키텍처 규칙에 따라 반환값 전달과 시스템 콜 번호 전달에 사용될 수 있음&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;크기 관계:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;RAX(64)&lt;/code&gt; -&amp;gt; &lt;code&gt;EAX(32)&lt;/code&gt; -&amp;gt; &lt;code&gt;AX(16)&lt;/code&gt; -&amp;gt; &lt;code&gt;AH/AL(8)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;시스템 콜 호출시 시스템 스코프 이동 블럭 다이어그램&lt;/h3&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Sonji-1/CS_study/main/Operating%20System/System_Call/Untitled%202.png&quot; alt=&quot;시스템 콜 흐름도&quot;&gt;&lt;/p&gt;
&lt;p&gt;흐름 요약:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;사용자 코드가 라이브러리 함수 호출&lt;/li&gt;
&lt;li&gt;라이브러리 래퍼가 시스템 콜 번호/인자 준비&lt;/li&gt;
&lt;li&gt;커널 모드 진입 후 시스템 콜 핸들러 실행&lt;/li&gt;
&lt;li&gt;핸들러가 번호를 기반으로 실제 커널 함수 호출&lt;/li&gt;
&lt;li&gt;필요하면 &lt;code&gt;copy_to_user()&lt;/code&gt; / &lt;code&gt;copy_from_user()&lt;/code&gt; 수행&lt;/li&gt;
&lt;li&gt;사용자 공간으로 복귀&lt;/li&gt;
&lt;/ol&gt;
&lt;ul&gt;
&lt;li&gt;시스템 콜 테이블은 보통 번호와 핸들러 매핑 정보를 가진다.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;예시 이미지:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Sonji-1/CS_study/main/Operating%20System/System_Call/Untitled%203.png&quot; alt=&quot;시스템 콜 테이블 예시&quot;&gt;&lt;/p&gt;
&lt;p&gt;매핑 테이블 참고: &lt;a href=&quot;https://blog.rchapman.org/posts/Linux_System_Call_Table_for_x86_64/&quot;&gt;Linux System Call Table for x86_64&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;sys_ni_syscall()&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;sys_ni_syscall()&lt;/code&gt;은 유효하지 않은 시스템 콜에 대해 &lt;code&gt;-ENOSYS&lt;/code&gt;를 반환한다.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/Sonji-1/CS_study/main/Operating%20System/System_Call/Untitled%204.png&quot; alt=&quot;sys_ni_syscall 소스&quot;&gt;&lt;/p&gt;
&lt;p&gt;출처: &lt;a href=&quot;https://elixir.bootlin.com/linux/latest/source/kernel/sys_ni.c#L20&quot;&gt;elixir.bootlin.com - kernel/sys_ni.c&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;capable() 리턴 정보&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;capable()&lt;/code&gt;에서 사용할 수 있는 권한(capability) 항목은 &lt;code&gt;&amp;lt;linux/capability.h&amp;gt;&lt;/code&gt;에서 확인 가능하다.&lt;/p&gt;
&lt;p&gt;참고: &lt;a href=&quot;https://sites.uclouvain.be/SystInfo/usr/include/linux/capability.h.html&quot;&gt;capability.h 문서&lt;/a&gt;&lt;/p&gt;</description>
      <category>Computer System/운영체제</category>
      <author>Sonji</author>
      <guid isPermaLink="true">https://sonlog.tistory.com/13</guid>
      <comments>https://sonlog.tistory.com/entry/OS-System-call#entry13comment</comments>
      <pubDate>Wed, 8 Apr 2026 16:32:01 +0900</pubDate>
    </item>
    <item>
      <title>[Effective C++] Cp 3. const 사용의 습관화</title>
      <link>https://sonlog.tistory.com/entry/Effective-C-Cp-3-const-%EC%82%AC%EC%9A%A9%EC%9D%98-%EC%8A%B5%EA%B4%80%ED%99%94</link>
      <description>&lt;h2&gt;1. 왜 const를 사용해야 하는가&lt;/h2&gt;
&lt;p&gt;const를 쓰는 이유는 간단하다. &lt;strong&gt;&amp;quot;바뀌면 안 되는 값을 못 바꾸게 만들기&amp;quot;&lt;/strong&gt; 위해서다.&lt;/p&gt;
&lt;h3&gt;의도의 전달&lt;/h3&gt;
&lt;p&gt;const를 붙이면 &amp;quot;이 값은 안 바뀐다&amp;quot;는 약속을 코드에 직접 적는 셈이다. 그래서 나중에 코드를 읽는 사람(미래의 나 포함)도 의도를 바로 이해할 수 있다.&lt;/p&gt;
&lt;h3&gt;컴파일러를 통한 실수 방지&lt;/h3&gt;
&lt;p&gt;const 값은 바꾸려고 하면 컴파일 단계에서 바로 에러가 난다. 프로그램을 실행하기 전에 실수를 잡을 수 있다는 뜻이다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;const int maxSize = 100;
maxSize = 200; // 컴파일 에러 → 실수를 즉시 발견&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;핵심은 &amp;quot;내가 조심하자&amp;quot;가 아니라 &amp;quot;컴파일러가 막아주게 하자&amp;quot;다. const를 붙일 수 있으면 가능한 붙이는 편이 안전하다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;2. const의 적용 범위&lt;/h2&gt;
&lt;p&gt;const는 생각보다 여러 곳에 적용할 수 있다.&lt;/p&gt;
&lt;h3&gt;2-1. 변수&lt;/h3&gt;
&lt;p&gt;가장 기본적인 사용법이다. 처음 정한 값이 이후에 바뀌면 안 될 때 쓴다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;const int bufferSize = 1024;
const std::string name = &amp;quot;sonji-log/tistory&amp;quot;;&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2-2. 포인터&lt;/h3&gt;
&lt;p&gt;포인터에서는 const를 어디에 붙이느냐가 중요하다. &amp;quot;가리키는 값&amp;quot;을 고정할지, &amp;quot;포인터 자체&amp;quot;를 고정할지가 달라진다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;int value = 10;

const int* p1 = &amp;amp;value;     // 가리키는 대상이 const (대상 변경 불가)
int* const p2 = &amp;amp;value;     // 포인터 자체가 const (다른 곳을 가리킬 수 없음)
const int* const p3 = &amp;amp;value; // 둘 다 const&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;쉽게 보면 &lt;code&gt;*&lt;/code&gt; 기준으로 읽으면 된다. &lt;strong&gt;왼쪽 const는 값 보호&lt;/strong&gt;, &lt;strong&gt;오른쪽 const는 포인터 보호&lt;/strong&gt;다.&lt;/p&gt;
&lt;h3&gt;2-3. 반복자(Iterator)&lt;/h3&gt;
&lt;p&gt;STL 반복자도 포인터와 비슷하게 생각하면 된다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;const std::vector&amp;lt;int&amp;gt;::iterator iter = vec.begin(); // iter는 다른 곳을 가리킬 수 없다 (T* const와 동일)
*iter = 10; // OK, 가리키는 값은 변경 가능

std::vector&amp;lt;int&amp;gt;::const_iterator cIter = vec.begin(); // 가리키는 값을 변경할 수 없다 (const T*와 동일)
*cIter = 10; // 컴파일 에러&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;2-4. 함수 매개변수&lt;/h3&gt;
&lt;p&gt;함수 안에서 인자를 바꾸지 않겠다는 뜻을 명확히 하고 싶을 때 사용한다. 특히 참조(&lt;code&gt;&amp;amp;&lt;/code&gt;)로 받을 때 자주 쓴다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;// 원본을 변경하지 않겠다는 보장
void print(const std::string&amp;amp; text)
{
    std::cout &amp;lt;&amp;lt; text &amp;lt;&amp;lt; std::endl;
    // text = &amp;quot;modified&amp;quot;; // 컴파일 에러
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;const 참조&lt;/code&gt;를 쓰면 복사는 줄이고, 원본은 보호할 수 있다. 그래서 기본 선택으로 많이 쓴다.&lt;/p&gt;
&lt;h3&gt;2-5. 함수 반환값&lt;/h3&gt;
&lt;p&gt;반환값에 const를 붙이면, 반환된 임시 값에 잘못 대입하는 실수를 막을 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class Rational { /* ... */ };

const Rational operator*(const Rational&amp;amp; lhs, const Rational&amp;amp; rhs);&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;예를 들어 아래 같은 실수를 컴파일 단계에서 걸러준다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;Rational a, b, c;

(a * b) = c;   // const 반환이므로 컴파일 에러
                // 의도는 비교(==)였을 가능성이 높다&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2&gt;3. const 멤버 함수&lt;/h2&gt;
&lt;h3&gt;3-1. 왜 멤버 함수에 const를 붙이는가&lt;/h3&gt;
&lt;p&gt;멤버 함수 뒤에 const를 붙이면 &amp;quot;이 함수는 객체 상태를 바꾸지 않는다&amp;quot;는 약속이 된다. 이 약속 덕분에 실제로 얻는 이점이 두 가지 있다.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;const 객체에서도 호출할 수 있다.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;함수 선언만 보고도 &amp;#39;상태 변경 여부&amp;#39;를 판단하기 쉽다.&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class TextBlock
{
public:
    // const 멤버 함수 — const 객체에서도 호출 가능
    const char&amp;amp; operator[](std::size_t position) const
    {
        return text[position];
    }

    // non-const 멤버 함수 — non-const 객체에서만 호출 가능
    char&amp;amp; operator[](std::size_t position)
    {
        return text[position];
    }

private:
    std::string text;
};&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;void print(const TextBlock&amp;amp; ctb)
{
    std::cout &amp;lt;&amp;lt; ctb[0]; // OK — const 버전 호출
    // ctb[0] = &amp;#39;x&amp;#39;;     // 컴파일 에러 — const char&amp;amp; 반환
}

TextBlock tb(&amp;quot;Hello&amp;quot;);
tb[0] = &amp;#39;J&amp;#39;;             // OK — non-const 버전 호출, char&amp;amp; 반환&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이런 const 오버로딩이 없으면, &lt;code&gt;print&lt;/code&gt;처럼 const 참조를 받는 코드에서 &lt;code&gt;operator[]&lt;/code&gt;를 못 쓰게 된다.&lt;/p&gt;
&lt;h3&gt;3-2. bitwise constness vs logical constness&lt;/h3&gt;
&lt;p&gt;const를 보는 기준은 두 가지가 있고, 둘이 항상 같지는 않다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;bitwise constness (물리적 상수성)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;컴파일러 기준의 상수성이다. 멤버 변수가 한 비트도 바뀌지 않으면 const라고 본다. 그런데 이 기준만으로는 놓치는 경우가 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class CTextBlock
{
public:
    char&amp;amp; operator[](std::size_t position) const
    {
        return pText[position]; // 포인터 자체는 안 바뀌니 컴파일 통과
    }

private:
    char* pText;
};

const CTextBlock cctb(&amp;quot;Hello&amp;quot;);
char* pc = &amp;amp;cctb[0];
*pc = &amp;#39;J&amp;#39;;  // const 객체의 내용이 바뀌어 버린다!&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;위 코드에서는 포인터 변수 자체는 그대로라서 컴파일러가 통과시킨다. 하지만 실제 문자열 내용은 바뀌었으니, 사람 입장에서는 &amp;quot;변경됨&amp;quot;이다.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;logical constness (논리적 상수성)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;프로그래머 기준의 상수성이다. &amp;quot;사용자 눈에 보이는 결과가 안 바뀌면 const로 보자&amp;quot;는 생각이다. 예를 들어 캐시 계산처럼 내부 최적화는 외부 동작이 같다면 const 함수 안에서 해도 괜찮다고 본다.&lt;/p&gt;
&lt;h3&gt;3-3. mutable 키워드&lt;/h3&gt;
&lt;p&gt;문제는 컴파일러가 bitwise 기준으로 검사한다는 점이다. 이때 &lt;code&gt;mutable&lt;/code&gt;이 도움이 된다. &lt;code&gt;mutable&lt;/code&gt; 멤버는 const 멤버 함수 안에서도 수정할 수 있다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class CTextBlock
{
public:
    std::size_t length() const
    {
        if (!lengthIsValid)
        {
            textLength = std::strlen(pText); // mutable이므로 변경 가능
            lengthIsValid = true;            // mutable이므로 변경 가능
        }
        return textLength;
    }

private:
    char* pText;
    mutable std::size_t textLength;  // const 멤버 함수에서도 수정 가능
    mutable bool lengthIsValid;      // const 멤버 함수에서도 수정 가능
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;length()&lt;/code&gt;는 바깥에서 볼 때 동작이 변하지 않는다. 내부 캐시만 갱신하는 것이므로 const로 두는 게 자연스럽고, &lt;code&gt;mutable&lt;/code&gt;이 그걸 가능하게 한다.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;4. const / non-const 중복 코드 제거&lt;/h2&gt;
&lt;h3&gt;4-1. 문제: 코드 중복&lt;/h3&gt;
&lt;p&gt;const 오버로딩을 하면 같은 코드가 두 번 들어가는 일이 많다. 경계 검사나 로그 같은 코드가 복붙되면 수정할 때 실수하기 쉽다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class TextBlock
{
public:
    const char&amp;amp; operator[](std::size_t position) const
    {
        // 경계 검사
        // 접근 로그 기록
        // 자료 무결성 검증
        return text[position];
    }

    char&amp;amp; operator[](std::size_t position)
    {
        // 경계 검사          ← 중복
        // 접근 로그 기록      ← 중복
        // 자료 무결성 검증    ← 중복
        return text[position];
    }

private:
    std::string text;
};&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;4-2. 해결: non-const가 const를 호출한다&lt;/h3&gt;
&lt;p&gt;해결 방법은 간단하다. non-const 버전에서 const 버전을 호출하고, 마지막에 const만 벗겨서 반환한다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class TextBlock
{
public:
    const char&amp;amp; operator[](std::size_t position) const
    {
        // 경계 검사, 로깅, 검증 등 모든 로직을 여기에 작성
        return text[position];
    }

    char&amp;amp; operator[](std::size_t position)
    {
        // 1. *this에 const를 붙여서 const 버전을 호출
        // 2. 반환된 const char&amp;amp;에서 const를 제거
        return const_cast&amp;lt;char&amp;amp;&amp;gt;(
            static_cast&amp;lt;const TextBlock&amp;amp;&amp;gt;(*this)[position]
        );
    }

private:
    std::string text;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;캐스트가 두 번 보여서 처음엔 낯설 수 있다. 그래도 핵심 로직을 한 곳에만 두게 되어 유지보수가 쉬워진다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;static_cast&amp;lt;const TextBlock&amp;amp;&amp;gt;(*this)&lt;/code&gt; — *this에 const를 붙여서 const 버전이 호출되도록 한다.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;const_cast&amp;lt;char&amp;amp;&amp;gt;(...)&lt;/code&gt; — const 버전이 반환한 &lt;code&gt;const char&amp;amp;&lt;/code&gt;에서 const를 벗겨낸다.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;4-3. 반대 방향은 안 되는 이유&lt;/h3&gt;
&lt;p&gt;반대로 const 버전에서 non-const 버전을 호출하면 위험하다. const 함수는 &amp;quot;안 바꾼다&amp;quot;가 약속인데, non-const 함수는 바꿀 수 있기 때문이다.&lt;br&gt;반면 non-const에서 const를 호출하는 건 안전하다. 원래 변경 권한이 있는 쪽에서 읽기 전용 버전을 재사용하는 형태이기 때문이다.&lt;/p&gt;
&lt;h3&gt;4-4. 그럼 const_cast를 직접 쓰면 안 될까?&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;const_cast&lt;/code&gt; 자체가 나쁜 것은 아니다. 다만 &lt;strong&gt;잘못 쓰면 const 약속을 깨기 쉬워서&lt;/strong&gt; 기본 전략으로는 권장되지 않는다.&lt;/p&gt;
&lt;p&gt;특히 아래처럼 const 함수 안에서 &lt;code&gt;const_cast&lt;/code&gt;로 non-const 함수를 부르는 방식은 위험하다.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-cpp&quot;&gt;class TextBlock
{
public:
    const char&amp;amp; operator[](std::size_t position) const
    {
        return const_cast&amp;lt;TextBlock&amp;amp;&amp;gt;(*this)[position]; // 권장하지 않음
    }

    char&amp;amp; operator[](std::size_t position)
    {
        return text[position];
    }

private:
    std::string text;
};&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;이 코드는 &amp;quot;const 객체&amp;quot;로 들어온 경우에도 non-const 경로를 탈 수 있어 문제가 생긴다.&lt;br&gt;즉, const 객체를 실제로 수정하면 &lt;strong&gt;정의되지 않은 동작(Undefined Behaviour)&lt;/strong&gt; 이 될 수 있다.&lt;/p&gt;
&lt;p&gt;정리하면 다음처럼 기억하면 된다.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;중복 제거의 기본 패턴은 &lt;code&gt;non-const -&amp;gt; const&lt;/code&gt; 호출&lt;/li&gt;
&lt;li&gt;&lt;code&gt;const_cast&lt;/code&gt;는 그 과정에서 &lt;strong&gt;반환 타입 맞출 때 최소한으로만&lt;/strong&gt; 사용&lt;/li&gt;
&lt;li&gt;const 객체를 수정할 가능성이 생기는 방향(&lt;code&gt;const -&amp;gt; non-const&lt;/code&gt;)은 피하기&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;5. 코멘트&lt;/h2&gt;
&lt;p&gt;이론으로는 참 많이 봤는데, 실제로 코드에서 잘 쓰느냐? 하면 모르겠다.&lt;br&gt;실제로 지금 개발 패러다임처럼 대부분 코드를 AI에게 맡기는 형국에선 const가 아예 없어서 QA 레벨에 많은 논리적 버그가 생기거나 const가 너무 많아서 개발자가 알아보기 힘든 문제가 같이 발생하는 것 같다.&lt;br&gt;개인적으로 AI의 코드를 리뷰할 때 const 를 사용한 방어처리가 많이 비어서 해당 로직을 추가해달라고 자주 요청했다. 결과적으로 내 서비스를 많이 deploy 해본 것은 아니라.. 얼마나 효용성이 있었는지는 검증되지 않았다.&lt;/p&gt;</description>
      <category>책/Effective C++</category>
      <category>C++</category>
      <author>Sonji</author>
      <guid isPermaLink="true">https://sonlog.tistory.com/12</guid>
      <comments>https://sonlog.tistory.com/entry/Effective-C-Cp-3-const-%EC%82%AC%EC%9A%A9%EC%9D%98-%EC%8A%B5%EA%B4%80%ED%99%94#entry12comment</comments>
      <pubDate>Wed, 8 Apr 2026 16:14:30 +0900</pubDate>
    </item>
    <item>
      <title>HackerRank C++ Gold Badge 달성</title>
      <link>https://sonlog.tistory.com/entry/HackerRank-C-Gold-Badge-%EB%8B%AC%EC%84%B1</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;341&quot; data-origin-height=&quot;362&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/rS9Fg/btsNbqKF2pW/ExeKTRYh4WdzqsWyJiI9zk/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/rS9Fg/btsNbqKF2pW/ExeKTRYh4WdzqsWyJiI9zk/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/rS9Fg/btsNbqKF2pW/ExeKTRYh4WdzqsWyJiI9zk/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FrS9Fg%2FbtsNbqKF2pW%2FExeKTRYh4WdzqsWyJiI9zk%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;341&quot; height=&quot;362&quot; data-origin-width=&quot;341&quot; data-origin-height=&quot;362&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HackerRank에서 C++문제를 조금 풀었더니 받은 뱃지.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;아무래도 Medium 레벨 문제부터 들어가서 경험치가 많이 찼던 것 같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;HackerRank 특성상 개수로만 밀어붙여도 꽤 많은 경험치를 얻을 수 있기 때문에 뱃지는 그냥 공부의식 고취를 위해서 주는 타이틀이 아닌가 싶다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;물론 그 목적에 완벽히 부합하게 받고 좋아했다.&lt;/p&gt;</description>
      <category>잡담</category>
      <category>C++</category>
      <category>hackerrank</category>
      <author>Sonji</author>
      <guid isPermaLink="true">https://sonlog.tistory.com/11</guid>
      <comments>https://sonlog.tistory.com/entry/HackerRank-C-Gold-Badge-%EB%8B%AC%EC%84%B1#entry11comment</comments>
      <pubDate>Mon, 7 Apr 2025 13:58:44 +0900</pubDate>
    </item>
    <item>
      <title>HackerRank Problem Solving (Basic) 달성</title>
      <link>https://sonlog.tistory.com/entry/HackerRank-Problem-Solving-Basic-%EB%8B%AC%EC%84%B1</link>
      <description>&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;847&quot; data-origin-height=&quot;631&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/eaBJLk/btsNbNr5O9K/lvG6niGBPX5KjotCOEcCmK/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/eaBJLk/btsNbNr5O9K/lvG6niGBPX5KjotCOEcCmK/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/eaBJLk/btsNbNr5O9K/lvG6niGBPX5KjotCOEcCmK/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FeaBJLk%2FbtsNbNr5O9K%2FlvG6niGBPX5KjotCOEcCmK%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;847&quot; height=&quot;631&quot; data-origin-width=&quot;847&quot; data-origin-height=&quot;631&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;간단한 알고리즘을 제한시간 내 성공적으로 풀이한 사람에게 주어지는 증명같다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;난이도가 어렵지 않다는 점만 기억나고, 실제로 무슨 문제가 나왔는지는 기억이 잘 나지 않는다(작성일 기준 약 3주가량 지났기 때문).&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;비록 굉장히 고차원적이고 어려운 증명은 아니었지만 무언가를 받았다는 점에 집중하기로 했다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>잡담</category>
      <category>hackerrank</category>
      <category>PS</category>
      <category>알고리즘</category>
      <category>해커랭크</category>
      <author>Sonji</author>
      <guid isPermaLink="true">https://sonlog.tistory.com/10</guid>
      <comments>https://sonlog.tistory.com/entry/HackerRank-Problem-Solving-Basic-%EB%8B%AC%EC%84%B1#entry10comment</comments>
      <pubDate>Mon, 7 Apr 2025 13:54:09 +0900</pubDate>
    </item>
    <item>
      <title>[프로그래머스] 디스크 컨트롤러 (Lv. 3) C++ 풀이</title>
      <link>https://sonlog.tistory.com/entry/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%94%94%EC%8A%A4%ED%81%AC-%EC%BB%A8%ED%8A%B8%EB%A1%A4%EB%9F%AC-Lv-3-C-%ED%92%80%EC%9D%B4</link>
      <description>&lt;h1&gt;문제 링크&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&lt;a title=&quot;디스크 컨트롤&quot; href=&quot;https://school.programmers.co.kr/learn/courses/30/lessons/42627&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://school.programmers.co.kr/learn/courses/30/lessons/42627&lt;/a&gt;&lt;/p&gt;
&lt;h1&gt;분류&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;Heap, SJF(Shortest Job First), Priority Queue&lt;/p&gt;
&lt;h1&gt;시도한 방법&lt;/h1&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제는가상의 스케줄러를 만드는 일이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;가정에서는 중간중간에 발생하는 Context Switching에 걸리는 시간이 없다고 명시되어 있으므로, 단순히 작업하는 데 걸리는 시간의 총합을 반환하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;문제의 핵심은 잔여시간이 긴 작업이 처리되고 있을 경우, 다른 작업들은 계속 기다리기만 해야 한다는 점이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이 부분은 전체 turn-around time을 크게 저해할 수 있으므로, 모든 작업이 끝났을 때 완료까지 걸리는 시간이 가장 짧은 작업을 우선해서 처리해야 한다고 해석할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 구조로 이루어진 알고리즘을 &lt;b&gt;Shortest Job First&lt;/b&gt; 라고 한다. &lt;a href=&quot;https://www.geeksforgeeks.org/shortest-job-first-or-sjf-cpu-scheduling/&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;(참고 링크)&lt;/a&gt;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;위 가정을 구현하는 가장 쉬운 방법은 &lt;b&gt;우선순위 큐&lt;/b&gt;를 활용하는 방법이다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;작업 대기 큐를 priority queue로 만들어서 작업에 소요되는 시간이 낮은 작업에 높은 우선순위를 보유하면 된다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;이렇게 디자인할 경우, 작업 대기 큐에 소요시간이 낮은 작업부터 적재되므로 따로 비교나 정렬연산을 작성해줄 필요 없이 문제를 해결할 수 있다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;당연하게도, 실제로 어떤 작업을 수행해야 할지는 관심가질 필요가 (여기서는)없기 때문에 지나고 있는 시간만 더해주면 답이 나올 수 있겠다.&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h1&gt;결과 코드&lt;/h1&gt;
&lt;pre id=&quot;code_1744000456476&quot; class=&quot;cpp&quot; data-ke-language=&quot;cpp&quot; data-ke-type=&quot;codeblock&quot;&gt;&lt;code&gt;#include &amp;lt;string&amp;gt;
#include &amp;lt;vector&amp;gt;
#include &amp;lt;queue&amp;gt;
#include &amp;lt;algorithm&amp;gt;

using namespace std;

struct process
{
    int id;
    int meaningTime;
    int inputTime;
    process(int id, int inputTime, int meaningTime) : id(id), inputTime(inputTime), meaningTime(meaningTime)  {}
};

bool compare(process a, process b)
{
    return a.inputTime &amp;lt; b.inputTime;
}

struct cmp
{
    bool operator()(process p1, process p2)
    {
        if (p1.meaningTime == p2.meaningTime)
        {
            if (p1.inputTime == p2.inputTime)
                return p1.id &amp;gt; p2.id;
            return p1.inputTime &amp;gt; p2.inputTime;
        }
        return p1.meaningTime &amp;gt; p2.meaningTime;
    }
};

bool operator&amp;lt;(process p1, process p2)
{
    if (p1.meaningTime == p2.meaningTime)
    {
        if (p1.inputTime == p2.inputTime)
            return p1.id &amp;gt; p2.id;
        return p1.inputTime &amp;gt; p2.inputTime;
    }
    return p1.meaningTime &amp;gt; p2.meaningTime;
}
int solution(vector&amp;lt;vector&amp;lt;int&amp;gt;&amp;gt; jobs)
{
    int answer = 0;
    priority_queue&amp;lt;process, vector&amp;lt;process&amp;gt;, cmp&amp;gt; readyQueue;
    vector&amp;lt;process&amp;gt; idle;

    for (int i = 0; i &amp;lt; jobs.size(); i++)
        idle.push_back(process(i, jobs[i][0], jobs[i][1]));

    int clock = 0;
    int averageTurnaroundTime = 0;
    int index = 0;
    sort(idle.begin(), idle.end(), compare);

    while (true)
    {
        if (index == idle.size() &amp;amp;&amp;amp; readyQueue.empty())
            break;

        while (index &amp;lt; idle.size() &amp;amp;&amp;amp; idle[index].inputTime &amp;lt;= clock)
        {
            readyQueue.push(idle[index]);
            index++;
        }

        if (readyQueue.empty())
        {
            clock = idle[index].inputTime;
            continue;
        }

        int turnaroundTime = 0;
        process running = readyQueue.top();
        readyQueue.pop();

        if (clock &amp;lt; running.inputTime)
            clock = running.inputTime;

        clock += running.meaningTime;
        turnaroundTime = clock - running.inputTime;
        averageTurnaroundTime += turnaroundTime;
    }

    answer = static_cast&amp;lt;int&amp;gt;(averageTurnaroundTime / jobs.size());

    return answer;
}&lt;/code&gt;&lt;/pre&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;
&lt;h2 data-ke-size=&quot;size26&quot;&gt;실행 결과&lt;/h2&gt;
&lt;p&gt;&lt;figure class=&quot;imageblock alignCenter&quot; data-ke-mobileStyle=&quot;widthOrigin&quot; data-origin-width=&quot;360&quot; data-origin-height=&quot;652&quot;&gt;&lt;span data-url=&quot;https://blog.kakaocdn.net/dn/bbgbIy/btsNaJjMiAs/3ldMOnuqXhgLozqhECxju1/img.png&quot; data-phocus=&quot;https://blog.kakaocdn.net/dn/bbgbIy/btsNaJjMiAs/3ldMOnuqXhgLozqhECxju1/img.png&quot;&gt;&lt;img src=&quot;https://blog.kakaocdn.net/dn/bbgbIy/btsNaJjMiAs/3ldMOnuqXhgLozqhECxju1/img.png&quot; srcset=&quot;https://img1.daumcdn.net/thumb/R1280x0/?scode=mtistory2&amp;fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbbgbIy%2FbtsNaJjMiAs%2F3ldMOnuqXhgLozqhECxju1%2Fimg.png&quot; onerror=&quot;this.onerror=null; this.src='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png'; this.srcset='//t1.daumcdn.net/tistory_admin/static/images/no-image-v1.png';&quot; loading=&quot;lazy&quot; width=&quot;360&quot; height=&quot;652&quot; data-origin-width=&quot;360&quot; data-origin-height=&quot;652&quot;/&gt;&lt;/span&gt;&lt;/figure&gt;
&lt;/p&gt;
&lt;p data-ke-size=&quot;size16&quot;&gt;&amp;nbsp;&lt;/p&gt;</description>
      <category>알고리즘 문제/Programmers</category>
      <category>C++</category>
      <category>SJF</category>
      <category>문제풀이</category>
      <category>스케줄링</category>
      <category>알고리즘</category>
      <category>우선순위큐</category>
      <category>운영체제</category>
      <category>코딩테스트</category>
      <category>프로그래머스</category>
      <author>Sonji</author>
      <guid isPermaLink="true">https://sonlog.tistory.com/9</guid>
      <comments>https://sonlog.tistory.com/entry/%ED%94%84%EB%A1%9C%EA%B7%B8%EB%9E%98%EB%A8%B8%EC%8A%A4-%EB%94%94%EC%8A%A4%ED%81%AC-%EC%BB%A8%ED%8A%B8%EB%A1%A4%EB%9F%AC-Lv-3-C-%ED%92%80%EC%9D%B4#entry9comment</comments>
      <pubDate>Mon, 7 Apr 2025 13:36:10 +0900</pubDate>
    </item>
  </channel>
</rss>