Study/TIL(Today I Learned)

24.09.04 CS, C++

에린_1 2024. 9. 4. 22:32
728x90

C++

스마트 포인터

  • 스마트 포인터(smart pointer)는 C++에서 동적 메모리 관리를 자동화하기 위해 사용되는 클래스 템플릿이다.
  • 스마트 포인터는 객체를 포인터처럼 사용하면서도 메모리 해제를 자동으로 처리하여 메모리 누수(memory leak)를 방지하는 데 도움을 준다. C++11부터 표준 라이브러리에서 제공하는 스마트 포인터에는 std::unique_ptr, std::shared_ptr, std::weak_ptr 등이 있다.

1. 스마트 포인터의 필요성

C++에서는 동적 메모리를 할당할 때 new 연산자를 사용하고, 할당된 메모리는 delete 연산자로 해제해야 한다. 하지만 메모리 해제를 깜빡하거나 예외가 발생할 경우, 메모리 누수가 발생할 수 있다. 스마트 포인터는 이러한 문제를 해결하기 위해 고안된 도구로, 소멸자에서 자동으로 메모리를 해제해 준다.

2. 주요 스마트 포인터 종류

  • std::unique_ptr는 유일 소유권(unique ownership)을 가지는 스마트 포인터이다. 하나의 unique_ptr만이 특정 메모리 자원을 소유할 수 있으며, 해당 포인터가 범위를 벗어날 때 메모리를 자동으로 해제한다.
    • 특징
      • 소유권을 가진 스마트 포인터가 소멸될 때 자동으로 delete 호출
      • 복사 불가능하지만, 이동 시 소유권 전송 가능 (std::move 사용)
      • 메모리 자원을 유일하게 소유하며, 다른 포인터와 자원을 공유할 수 없음
    • 사용 예시
    • cpp코드 복사 #include <iostream>#include <memory>void example() { std::unique_ptr<int> ptr1(new int(10)); // unique_ptr 생성 std::cout << *ptr1 << std::endl; // 출력: 10 std::unique_ptr<int> ptr2 = std::move(ptr1); // ptr1의 소유권을 ptr2로 이동 if (!ptr1) { std::cout << "ptr1 is null" << std::endl; // 출력: ptr1 is null } std::cout << *ptr2 << std::endl; // 출력: 10 } // ptr2가 소멸되면서 자동으로 메모리 해제
    • 적용 사례
      • 특정 자원이 하나의 객체에 의해서만 관리될 때
      • 소유권을 명확하게 관리하고 싶을 때
  • std::shared_ptr는 공유 소유권(shared ownership)을 가지는 스마트 포인터이다. 여러 개의 shared_ptr가 같은 메모리를 가리킬 수 있으며, 모든 shared_ptr가 소멸되었을 때 메모리를 해제한다. std::shared_ptr는 내부적으로 참조 카운트(reference count)를 유지하여 소유권을 관리한다.
    • 특징:
      • 참조 카운트를 사용해 여러 포인터가 동일 자원을 공유
      • 마지막 shared_ptr가 소멸될 때 메모리 해제
      • std::make_shared를 사용하면 더 안전하고 효율적으로 shared_ptr 생성 가능
    • 사용 예시
    • cpp코드 복사 #include <iostream>#include <memory>void example() { std::shared_ptr<int> ptr1 = std::make_shared<int>(20); // shared_ptr 생성 std::shared_ptr<int> ptr2 = ptr1; // ptr1과 ptr2가 같은 메모리를 공유 std::cout << *ptr1 << ", " << *ptr2 << std::endl; // 출력: 20, 20 std::cout << "Reference Count: " << ptr1.use_count() << std::endl; // 출력: 2 ptr1.reset(); // ptr1의 소유권 해제 std::cout << "Reference Count after ptr1 reset: " << ptr2.use_count() << std::endl; // 출력: 1 } // ptr2가 소멸되면서 메모리 해제
    • 적용 사례
      • 같은 자원이 여러 곳에서 사용되고, 수명이 동적으로 관리되어야 할 때
      • 객체의 수명이 동적으로 결정될 때
  • std::weak_ptr는 std::shared_ptr와 함께 사용되며, 약한 참조(weak reference)를 제공하는 스마트 포인터이다. weak_ptr는 shared_ptr가 관리하는 메모리를 참조할 수 있지만, 참조 카운트를 증가시키지 않는다. 따라서 weak_ptr는 객체의 수명에는 영향을 미치지 않는다.
    • 특징:
      • 참조 카운트를 증가시키지 않음
      • 메모리가 해제된 후에도 weak_ptr는 유효하므로, 사용하기 전에 expired() 메서드를 통해 유효성 검사를 해야 함
      • lock() 메서드를 사용해 std::shared_ptr로 변환하여 안전하게 사용할 수 있음
    • 사용 예시:
    • cpp코드 복사 #include <iostream>#include <memory>void example() { std::shared_ptr<int> sharedPtr = std::make_shared<int>(30); std::weak_ptr<int> weakPtr = sharedPtr; // weak_ptr 생성, 참조 카운트는 증가하지 않음 std::cout << "Reference Count: " << sharedPtr.use_count() << std::endl; // 출력: 1 if (auto lockedPtr = weakPtr.lock()) { // shared_ptr로 변환 std::cout << "Value: " << *lockedPtr << std::endl; // 출력: 30 } sharedPtr.reset(); // 메모리 해제 if (weakPtr.expired()) { std::cout << "The weak pointer is expired." << std::endl; // 출력: The weak pointer is expired. } }
    • 적용 사례:
      • 순환 참조(circular reference) 문제를 방지하고 싶을 때
      • 자원의 생명 주기를 관리하면서도 소유권을 갖지 않아야 할 때

3. 스마트 포인터의 비교와 선택

스마트 포인터 소유권 사용 사례 특징

std::unique_ptr 유일 소유권 자원이 단일 객체에 의해 소유될 때 복사 불가능, 이동 가능
std::shared_ptr 공유 소유권 여러 객체가 자원을 공유하고, 참조 카운트를 통해 자원을 관리할 때 참조 카운트를 사용해 자원 관리, 복사 및 이동 가능
std::weak_ptr 약한 참조(소유권 없음) shared_ptr와의 순환 참조 방지 참조 카운트를 증가시키지 않음, expired()와 lock() 메서드 제공

4. 순환 참조 문제와 해결 방법

  • std::shared_ptr를 사용할 때 순환 참조(circular reference) 문제가 발생할 수 있다. 두 객체가 서로를 shared_ptr로 참조하면 참조 카운트가 0이 되지 않아 메모리가 해제되지 않는다. 이 문제를 해결하기 위해 std::weak_ptr를 사용한다.
    • 예시:
    • cpp코드 복사 #include <memory>class B; // 전방 선언 class A { public: std::shared_ptr<B> b_ptr; ~A() { std::cout << "A destroyed" << std::endl; } }; class B { public: std::weak_ptr<A> a_ptr; // weak_ptr로 순환 참조 방지 ~B() { std::cout << "B destroyed" << std::endl; } }; void circular_reference_example() { auto a = std::make_shared<A>(); auto b = std::make_shared<B>(); a->b_ptr = b; b->a_ptr = a; // weak_ptr로 설정하여 순환 참조 방지 }

인라인 함수(Inline Function)

  • 인라인 함수(inline function)는 C++에서 컴파일러에게 함수 호출을 최적화하도록 제안하는 함수다. 일반적으로 함수를 호출하면 함수의 호출 과정(스택 프레임 생성, 매개변수 전달 등)으로 인해 오버헤드가 발생한다.
  • 하지만 인라인 함수를 사용하면, 컴파일러가 함수 호출을 제거하고 해당 함수의 코드를 호출 지점에 직접 삽입하여 이러한 오버헤드를 줄일 수 있다. 이를 통해 프로그램 성능을 향상시킬 수 있다.

1. 인라인 함수의 개념

  • 인라인 함수는 inline 키워드를 사용하여 정의된다. 인라인이라는 이름에서 알 수 있듯이, 컴파일러가 함수 호출을 인라인으로 확장하라는 힌트를 주는 것이다. 즉, 함수의 호출을 함수 본체의 코드로 대체한다. 이를 통해 함수 호출에 따른 오버헤드를 제거할 수 있다.
  • 인라인 함수의 정의
    • 위 함수는 인라인 함수로 정의되었으며, 컴파일러는 이 함수를 호출할 때 add(a, b)를 a + b로 대체할 수 있다.
  • cpp코드 복사 inline int add(int a, int b) { return a + b; }

2. 인라인 함수의 특징

  • 함수 호출 오버헤드를 줄임
    • 함수 호출을 실제 코드로 대체하기 때문에 호출 오버헤드가 사라진다.
  • 컴파일러가 반드시 인라인 확장을 보장하지는 않음
    • inline은 단지 컴파일러에 대한 제안이며, 컴파일러가 함수가 너무 복잡하거나 크다고 판단하면 인라인 확장을 무시할 수 있다.
  • 짧고 빈번히 호출되는 함수에 적합
    • 인라인 함수는 보통 짧고 간단한 함수에 적합하다. 복잡한 함수에 인라인을 사용하면 오히려 코드 크기가 증가하여 성능이 저하될 수 있다.
  • 디버깅의 어려움
    • 인라인 함수는 코드가 여러 번 복사되어 삽입되기 때문에, 디버깅 시 각 호출 지점에서의 문제를 추적하기 어려울 수 있다.
  • 코드 크기 증가 가능성
    • 여러 번 호출되는 함수가 인라인으로 확장되면, 코드 크기가 증가할 수 있다. 이는 캐시 미스(cache miss)를 유발할 수 있고, 성능 저하로 이어질 수 있다.

3. 인라인 함수 사용 방법

  • inline 키워드 사용
    • 인라인 함수는 inline 키워드를 사용하여 선언하고 정의한다. 함수 선언부와 정의부가 같은 파일에 있어야 하며, 주로 헤더 파일에 정의된다.
    • cpp코드 복사 // Example.h inline int multiply(int x, int y) { return x * y; }
    • 헤더 파일에 정의된 인라인 함수는 #include 지시자로 다른 소스 파일에서 사용할 수 있다. 이 경우, 컴파일러가 인라인 확장을 수행하여 성능을 최적화할 수 있다.
  • 클래스 멤버 함수와 인라인
    • 클래스 내부에 정의된 멤버 함수는 자동으로 인라인 함수로 간주된다. 이는 작은 멤버 함수의 경우 자주 사용되며, 클래스 헤더 파일에 정의된다.
    • cpp코드 복사 class MyClass { public: int getValue() const { // 인라인 멤버 함수 return value; } private: int value; };
    • 위 예제에서 getValue() 함수는 클래스 내부에서 정의되었기 때문에 자동으로 인라인 함수가 된다.

4. 인라인 함수의 장점과 단점

  • 장점
    1. 성능 향상
      • 작은 함수의 경우 호출 오버헤드를 줄임으로써 성능이 향상된다.
    2. 코드 간결화
      • 매크로보다 안전하게 코드를 간결하게 표현할 수 있다. 인라인 함수는 타입 검사를 받기 때문에 매크로보다 타입 안전성이 높다.
    3. 간단한 구현
      • 간단한 함수에 대해 인라인으로 최적화할 수 있는 방법을 제공한다.
  • 단점
    1. 코드 크기 증가
      • 인라인 함수가 여러 번 호출되면 그만큼 코드가 중복 삽입되어 바이너리 크기가 증가할 수 있다.
    2. 디버깅 어려움
      • 인라인 확장으로 인해 디버깅 시 정확한 위치 추적이 어려워질 수 있다.
    3. 복잡한 함수에는 부적합
      • 복잡하고 큰 함수에 대해 인라인 확장을 시도하면 오히려 성능이 저하될 수 있다.

5. 인라인 함수와 매크로 비교

  • C++에서 함수와 유사한 작업을 수행하기 위해 매크로(#define)를 사용할 수 있다. 하지만 매크로는 인라인 함수와 비교해 여러 단점이 있다.
    • 안전성
      • 매크로는 타입 안전성을 보장하지 않으며, 괄호가 제대로 사용되지 않으면 의도하지 않은 결과를 초래할 수 있다. 인라인 함수는 C++의 타입 검사와 관련된 모든 규칙을 따르므로 더 안전하다.
    • 디버깅 지원
      • 인라인 함수는 디버거에서 함수처럼 추적할 수 있지만, 매크로는 그렇지 않다.
    • 코드 가독성
      • 매크로는 복잡해지면 읽기 어렵고 유지보수가 어려워질 수 있다.
  • cpp코드 복사 #define SQUARE(x) ((x) * (x)) // 매크로 정의inline int square(int x) { // 인라인 함수 정의 return x * x; }

6. 컴파일러의 인라인 확장 판단 기준

  • 컴파일러는 다음과 같은 경우에 inline 키워드를 무시할 수 있다
    1. 함수가 너무 길거나 복잡한 경우
      • 큰 함수의 경우 코드 크기 증가로 인한 성능 저하가 발생할 수 있다.
    2. 재귀 함수인 경우
      • 재귀 함수는 함수 호출이 반복적으로 발생하므로 인라인 확장이 불가능하다.
    3. 함수가 가상 함수인 경우
      • 가상 함수는 런타임에 결정되기 때문에 컴파일 타임에 인라인 확장이 어렵다.
    4. 함수가 루프 내부에서 호출되는 경우
      • 루프 내에서 자주 호출되는 큰 함수는 인라인 확장 시 성능 저하를 초래할 수 있다.

7. inline과 constexpr의 관계

  • C++11부터는 constexpr 키워드가 도입되면서, 상수 표현식(constant expression)을 컴파일 타임에 계산할 수 있게 되었다. constexpr 함수는 자동으로 인라인 함수가 되며, 컴파일 타임 최적화가 가능하다.
  • cpp코드 복사 constexpr int add(int a, int b) { return a + b; }
  • constexpr 함수는 컴파일 타임에 값을 계산할 수 있으며, 최적화에도 기여할 수 있다.
728x90

'Study > TIL(Today I Learned)' 카테고리의 다른 글

24.09.06 CSAPP, CS  (4) 2024.09.06
24.09.05 CSAPP복습 ,C++  (0) 2024.09.05
24.09.03 CS, C++  (0) 2024.09.03
24.09.02 CS, 언리얼  (1) 2024.09.02
24.08.30 CS, C++  (0) 2024.08.30