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. 인라인 함수의 장점과 단점
- 장점
- 성능 향상
- 작은 함수의 경우 호출 오버헤드를 줄임으로써 성능이 향상된다.
- 코드 간결화
- 매크로보다 안전하게 코드를 간결하게 표현할 수 있다. 인라인 함수는 타입 검사를 받기 때문에 매크로보다 타입 안전성이 높다.
- 간단한 구현
- 간단한 함수에 대해 인라인으로 최적화할 수 있는 방법을 제공한다.
- 성능 향상
- 단점
- 코드 크기 증가
- 인라인 함수가 여러 번 호출되면 그만큼 코드가 중복 삽입되어 바이너리 크기가 증가할 수 있다.
- 디버깅 어려움
- 인라인 확장으로 인해 디버깅 시 정확한 위치 추적이 어려워질 수 있다.
- 복잡한 함수에는 부적합
- 복잡하고 큰 함수에 대해 인라인 확장을 시도하면 오히려 성능이 저하될 수 있다.
- 코드 크기 증가
5. 인라인 함수와 매크로 비교
- C++에서 함수와 유사한 작업을 수행하기 위해 매크로(#define)를 사용할 수 있다. 하지만 매크로는 인라인 함수와 비교해 여러 단점이 있다.
- 안전성
- 매크로는 타입 안전성을 보장하지 않으며, 괄호가 제대로 사용되지 않으면 의도하지 않은 결과를 초래할 수 있다. 인라인 함수는 C++의 타입 검사와 관련된 모든 규칙을 따르므로 더 안전하다.
- 디버깅 지원
- 인라인 함수는 디버거에서 함수처럼 추적할 수 있지만, 매크로는 그렇지 않다.
- 코드 가독성
- 매크로는 복잡해지면 읽기 어렵고 유지보수가 어려워질 수 있다.
- 안전성
- cpp코드 복사 #define SQUARE(x) ((x) * (x)) // 매크로 정의inline int square(int x) { // 인라인 함수 정의 return x * x; }
6. 컴파일러의 인라인 확장 판단 기준
- 컴파일러는 다음과 같은 경우에 inline 키워드를 무시할 수 있다
- 함수가 너무 길거나 복잡한 경우
- 큰 함수의 경우 코드 크기 증가로 인한 성능 저하가 발생할 수 있다.
- 재귀 함수인 경우
- 재귀 함수는 함수 호출이 반복적으로 발생하므로 인라인 확장이 불가능하다.
- 함수가 가상 함수인 경우
- 가상 함수는 런타임에 결정되기 때문에 컴파일 타임에 인라인 확장이 어렵다.
- 함수가 루프 내부에서 호출되는 경우
- 루프 내에서 자주 호출되는 큰 함수는 인라인 확장 시 성능 저하를 초래할 수 있다.
- 함수가 너무 길거나 복잡한 경우
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 |