내일배움캠프/TIL

TIL260430 - CPP

옆집히드라 2026. 4. 30. 20:59

1. 핵심 개념


  • 스마트 포인터
  • map.end()는 없는 값 Past-the-end-iterator
  • find, count, all_of ~ algorithm 차이
  • 멤버 초기화 리스트는 실제 코드보다 먼저 실행됨
  • 가상함수 테이블이란? 가상함수 테이블의 오버헤드는? 동적바인딩, 동적 디스패치... 동적 바인딩을 알리는걸까?, 순수가상함수(하나 이상 포함 시 C++은 추상 클래스로 봄), 순수 가상함수 override 안해주면 상속해도 똑같이 추상 클래스; override 안붙여도 동작하나 에러 방지를 위해 반드시 붙여줄 것
  • 자식 클래스의 소멸자는 virtual 클래스 붙여줄 것(안붙이면 부모 소멸자 호출)
  • pure virtual destructor: 순수 가상 소멸자를 만드는 이유는 부모 클래스에 마땅히 추상클래스로 만들만한 메서드가 없어서 사용 단, 가상 소멸자를 넣은 경우 외부에서 정의 해줘야 링크 에러가 안뜸; 순수 가상함수는 소멸자 필요.
    • If you create an object with default implementations for its virtual methods and want to make it abstract without forcing anyone to override any specific method, you can make the destructor pure virtual. I don't see much point in it but it's possible.
    • 스택오버플로 보면 1. Probably the real reason that pure virtual destructors are allowed is that to prohibit them would mean adding another rule to the language and there's no need for this rule since no ill-effects can come from allowing a pure virtual destructor.로 별 의미없는 문법이라는 의견이 지지받고 있음 아마도 특별히 사용할 일은 없어보인다.
  • C++에 모든 타입에 초기화는 균일 초기화({ })로 통일해서 사용 가능(C++11 이상), 균일 초기화를 사용하면 축소 변환(narrowing)을 방지할 수 있다.(축소 변환 캐스트를 원한다면 gsl::narrow_cast() 사용); 변수 초기화에는 대입 구문보다 균일 초기화가 바람직함
  • cpp20에서 지정 초기자(designated initializer) 도입됨 묶음 타입(aggregate type)의 데이터 멤버를 초기화할 때 사용하며, 지정 초기자로 초기화되지 않은 멤버들을 내부 초기자가 있으면 내부 초기자로, 없다면 0으로 초기화 해준다.

2. 상세 내용


2.1. 스마트 포인터


기존 C 스타일 포인터는 할당과 해제를 모두 프로그래머가 도맡아 하는 탓에 잘못된 사용으로 이미 제거된 객체를 가리키는 포인터를 참조하는 Dangling pointer나 프리스토어(Heap)에서 해제 되지 못한 객체가 쌓이는 Memory leak 같은 문제가 발생하기 쉽다.

C++에서 권장하는 방식은 리소스를 할당할 때 memory 라이브러리의 스마트 포인터를 사용하는 것이다. 결과를 unique_ptr(단일), shared_ptr(공유)에 저장하거나 RAII(Resource Acquisition Is Initialization class를 사용하는 것이다.

2.1.1. unique_ptr

unique_ptr은 단독 소유권을 제공한다. unique_ptr은 오직 하나의 리소스만이 소유할 수 있으며 스코프를 벗어나거나 익셉션이 발생했을 때 자동으로 해제된다. 만약 unique_ptr의 소유권을 이전하고 싶다면 std::move를 사용한다.(인자로 받은 객체를 R-Value로 캐스팅하여 L-Value로 이동)

#include <iostream>
#include <memory>

class MyClass {
public:
    MyClass() { std::cout << "객체 생성\n"; }
    ~MyClass() { std::cout << "객체 소멸 (자동 해제)\n"; }
    void doSomething() { std::cout << "작업 수행\n"; }
};

int main() {
    // 1. 할당 (make_unique 사용 권장)
    std::unique_ptr<MyClass> ptr1 = std::make_unique<MyClass>();

    // std::unique_ptr<MyClass> ptr2 = ptr1; // 컴파일 에러: 복사 불가

    // 2. 소유권 이전 (std::move 사용)
    std::unique_ptr<MyClass> ptr2 = std::move(ptr1);

    if (!ptr1) {
        std::cout << "ptr1의 소유권이 이전되어 비어있습니다.\n";
    }

    if (ptr2) {
        ptr2->doSomething();
    }

    // 3. 해제: main 함수의 스코프를 벗어날 때 ptr2가 파괴되면서 
    // 내부적으로 MyClass의 소멸자가 자동으로 호출됨
    return 0;
}

2.1.2. shared_ptr

shared_ptr은 레퍼런스 카운트 방식을 사용하여 포인터를 공유한다. shared_ptr을 참조하는 포인터가 제거되어 레퍼런스 카운트가 0(아무도 참조 안함)이 되면 자동으로 해제된다.

shared_ptr인 두 스마트 포인터가 서로를 참조하는 경우 순환 참조가 발생해 레퍼런스 카운트가 서로 감소하지 않는 교착 상태가 될 수 있다. 이 경우 둘 중하나를 weak_ptr로 변경하면 해결할 수 있다.

#include <iostream>
#include <memory>

struct B; // 전방 선언

struct A {
    std::shared_ptr<B> b_ptr;
    A() { std::cout << "A 생성\n"; }
    ~A() { std::cout << "A 소멸\n"; } // 순환 참조 시 호출되지 않음
};

struct B {
    std::shared_ptr<A> a_ptr;
    B() { std::cout << "B 생성\n"; }
    ~B() { std::cout << "B 소멸\n"; } // 순환 참조 시 호출되지 않음
};

int main() {
    {
        std::shared_ptr<A> a = std::make_shared<A>();
        std::shared_ptr<B> b = std::make_shared<B>();

        std::cout << "초기 a 참조 카운트: " << a.use_count() << "\n"; // 1
        std::cout << "초기 b 참조 카운트: " << b.use_count() << "\n"; // 1

        // 순환 참조 발생
        a->b_ptr = b;
        b->a_ptr = a;

        std::cout << "연결 후 a 참조 카운트: " << a.use_count() << "\n"; // 2
        std::cout << "연결 후 b 참조 카운트: " << b.use_count() << "\n"; // 2
    }
    // 스코프 종료: a와 b의 로컬 변수는 파괴되어 카운트가 1씩 감소하지만,
    // 객체 내부의 멤버 변수가 서로를 가리키고 있어 카운트가 1로 남음.
    // 결과적으로 소멸자가 호출되지 않아 메모리 누수 발생.

    std::cout << "스코프 종료됨 (소멸자 호출 안 됨)\n";
    return 0;
}

2.1.3. weak_ptr

weak_ptr은 포인터를 공유하지만 레퍼런스 카운트를 가지지 않는다. 따라서 weak_ptr은 null일 수 있으므로 사용전에 lock() 함수로 내부 객체 유효성을 확인해야 한다.

#include <iostream>
#include <memory>

struct B_Solved;

struct A_Solved {
    std::shared_ptr<B_Solved> b_ptr;
    A_Solved() { std::cout << "A_Solved 생성\n"; }
    ~A_Solved() { std::cout << "A_Solved 소멸\n"; }
};

struct B_Solved {
    // shared_ptr 대신 weak_ptr을 사용하여 순환 참조 방지
    std::weak_ptr<A_Solved> a_ptr; 

    B_Solved() { std::cout << "B_Solved 생성\n"; }
    ~B_Solved() { std::cout << "B_Solved 소멸\n"; }

    void useA() {
        // weak_ptr 사용 전 lock()으로 객체 생존 여부 확인
        if (std::shared_ptr<A_Solved> shared_a = a_ptr.lock()) {
            std::cout << "A 객체 접근 성공. 현재 A 참조 카운트: " << shared_a.use_count() << "\n";
        } else {
            std::cout << "A 객체가 이미 소멸되었습니다.\n";
        }
    }
};

int main() {
    {
        std::shared_ptr<A_Solved> a = std::make_shared<A_Solved>();
        std::shared_ptr<B_Solved> b = std::make_shared<B_Solved>();

        a->b_ptr = b;
        b->a_ptr = a; // weak_ptr이므로 a의 참조 카운트를 증가시키지 않음

        std::cout << "연결 후 a 참조 카운트: " << a.use_count() << "\n"; // 1 유지

        b->useA(); // lock() 성공
    }
    // 스코프 종료:
    // 1. a가 파괴되면서 A 객체의 카운트가 0이 되어 A 객체 소멸.
    // 2. A가 소멸되면서 내부의 b_ptr(shared_ptr)도 파괴되어 B 객체의 카운트가 0이 됨. B 객체 소멸.
    // 정상적으로 모든 메모리가 해제됨.

    std::cout << "스코프 종료됨 (정상 소멸)\n";
    return 0;
}

3. 질문 및 해결 (Q&A)


c++ 문법 공부 점검용 체크리스트(C# 기준)

object slicing? move semantics?

  • oop
    • 클래스
    • 생성자(복제, 대입), 소멸자
    • 접근 제한자
    • friend
    • 상속 + 상속 접근 제한자 + virtual 상속(다중 상속 문제 해결) + final
    • 인터페이스 & 추상클래스(순수 가상함수)
    • 다형성(virtual)
    • 오버로딩(함수, 연산자)
    • 클래스 내 static 변수
    • 캐스팅(업, 다운, C++ 스타일)
    • 공변성 & 반공변성(매개변수는 서브타입, 반환은 베이스타입으로 허용)
  • 메모리 기타
    • C 스타일 포인터
    • 레퍼런스
    • const
    • refernce & dereference
  • cpp 관련
    • L-Value & R-Value
  • STL
    • 이터레이터
    • 튜플
    • 동적 배열
    • 연결 리스트, 순환 연결 리스트
    • 해시맵
    • 트리 구조
  • 알고리즘
    • 정렬
    • math
  • 비동기 프로그래밍
    • 스레드
    • 아토믹 연산
    • 세마포어, 모니터, 스핀락
  • 디자인 패턴 기법(cpp 버전)
    • 추상 팩토리
    • 팩토리 메서드
    • 어댑터
    • 프록시
    • 반복자
    • 옵저버
    • 데코레이터
    • 책임 사슬
    • 싱글턴(private 생성자 구현)
  • 서버 프로그래밍
  • 템플릿
  • 스트링 처리
  • 익셉션 처리
  • 링크와 컴파일
  • fp
    • 람다
    • lazy evaluation 가능한 LINQ 같은 것
    • 메서드 체이닝
    • 커링

4. 관련 문서 (Links)


'내일배움캠프 > TIL' 카테고리의 다른 글

TIL260506 - Quaternion  (0) 2026.05.06
TIL260504 - Quaternion  (0) 2026.05.04
TIL260429 - CPP  (0) 2026.04.29
TIL260428 - Unreal  (0) 2026.04.28
TIL260427 - CPP  (0) 2026.04.27