본문 바로가기
공부

[C, C++] 허상포인터(Dangling Pointer) + 와일드포인터(Wild Pointer) + 스마트포인터

by MY블로그 2023. 5. 3.

와일드포인터(Wild Pointer) 

처음 사용시 초기화를 하지 않았을때 발생합니다.

초기화가 강요되지 않는 모든 포인터는 와일드 포인터로 시작됩니다.

대부분 초기화의 생략 보다는 깜빡하고 초기화하지 않아 발생하는 경우가 많고 컴파일러는 이에 대하여 경고 할 수 있다고 합니다.

 

허상포인터(Dangling Pointer)

동적 할당 된 포인터를 delete 혹은 free 를 사용하여 해제 할 경우

할당되었던 메모리 영역은 해제가 되지만 가리키고 있던 포인터 자체까지 삭제되지는 않습니다.

때문에 목표를 잃은 채로 존재하고 있는 포인터가 즉, 허상 포인터가 됩니다.

#include <iostream>
using namespace std;

int main()
{
    int* ptr = new int();
    *ptr = 5;
    cout << *ptr << endl;
    cout << ptr << endl;
    delete ptr; // ptr은 댕글링 포인터

    int a;
    cin >> a;
    return 0;
}

위처럼 댕글링 포인터가 있을 경우 어떠한 문제점이 있을 까요?

  • 메모리 해제 후에 다시 해제된 메모리에 접근할 때
  • 함수 호출에서 자동 변수를 가리키고있는 포인터를 반환할 때

의 경우에 문제가 생길 수 있습니다.

예를들어 delete ptr 이후에 *ptr 에 새로운 값을 넣는다면 오류가 생기게 됩니다.

이렇게 될 경우 다양한 문제가 생길 수 있습니다.

포인터 역참조를 할때에 예측 불가능한 상황이 생기거나 프로그램의 실행 또는 진행에 있어 큰 오류가 생길 수 있습니다. 이는 곧 잠재적인 보안 위험에 연결 됩니다.

 

해결 방법

  • free 혹은 delete 직후 바로 ptr = NULL; 을 대입하여 비워줍니다.
  • 매크로를 사용합니다.
  • C스타일 매크로 #define FREE(ptr) if(ptr) {free(ptr); ptr = NULL;}
  • C++스타일 에서는 SAFE_DELETE 형태의 매크로를 사용할 수 있습니다.(아래코드참조)
#define SAFE_DELETE(p)       { if(p) { delete (p); (p)=0; } }
#define SAFE_DELETE_ARRAY(p) { if(p) { delete[] (p);   (p)=0; } }
#define SAFE_RELEASE(p)      { if(p) { (p)->Release(); (p)=0; } }

 


 

추가적으로 C++에서스마트포인터의 기능을 제공하고 있습니다.

스마트포인터(Smart Pointer) 

C++ 에서 new 를 사용하여 동적할당 받은 메모리는 반드시 delete 를 사용하여 해제하는것은 이제 기본적으로 알고 있습니다.

하지만 위처럼 사람이 하는 일에 완벽한일은 없으니 메모리 누수로부터 조금더 안전할 수 있도록 제공되는 기능이 스마트포인터 입니다.

좀더 고수준의 언어 프로그램에서는 가비지컬렉터가 있으나 C++에는 없습니다.

그렇기때문에 C++에서는 포인터처럼 동작하는 클래스 템플릿을 통하여 사용이 끝난 메모리를 자동적으로 해제할 수 있도록 합니다.

 

보통 new를 사용해 기본 포인터가 실제 메모리를 가리키도록 초기화한 후에 기본 포인터를 스마트 포인터에 대입하여 사용하도록 동작합니다. (복습, 초기화 안하면 와일드포인터!)

정의된 스마트 포인터가 작업을 마칠때가 되면 소멸자는 delete 키워드를 사용하여 할당된 메모리를 자동적으로 해재할 수 있습니다.

즉, 소멸자에 delete 기능 탑재! 소멸자를 통하여 사용하기때문에 소멸자가 없는 C에는 스마트포인터 X

따라서 new 가 반환하는 주소값을 스마트 포인터에 대입하면 따로 해제할 필요없이 자동적으로 해제되는 것 입니다.

 

이러한 스마트 포인터에도 다양한 종류가 있습니다.

  • unique_ptr
  • shared_ptr
  • weak_ptr

스마트 포인터는 memory 헤더파일에 정의 되어 있습니다.

unique_ptr 

하나의 스마트 포인터만이 특정 객체를 소유할 수 있도록 객체에 소유권 개념을 갖습니다.

해당 객체의 소유관을 가지고 있을 때만, 소멸자가 해당 객체를 삭제 할 수 있습니다.

인스턴스는 move() 멤버 함수를 통하여 소유권을 이동시킬 수 있습니다. (복사불가)

소유권이 이전되면, 이전의 인스턴스는 더이상 해당 객체를 소유하지 않게 재설정 됩니다.

C++14 이후부터는 make_unique() 함수를 사용하여 unique_ptr을 생성할 수 있습니다.

unique_ptr<int> ptr01(new int(5)); // int형 unique_ptr인 ptr01을 선언하고 초기화함.
auto ptr02 = move(ptr01);          // ptr01에서 ptr02로 소유권을 이전함.
// unique_ptr<int> ptr03 = ptr01;  // 대입 연산자를 이용한 복사는 오류를 발생시킴. 
ptr02.reset();                     // ptr02가 가리키고 있는 메모리 영역을 삭제함.
ptr01.reset();                     // ptr01가 가리키고 있는 메모리 영역을 삭제함.

다음의 예제는 Person 객체를 가리키는 hong이라는 unique_ptr를 make_unique()르 통하여 생성합니다.

#include <iostream>
#include <memory> // 스마트 포인터 사용일 위한 헤더
using namespace std;

class Person
{
private:
    string name_;
    int age_;
public:
    Person(const string& name, int age); // 기초 클래스 생성자의 선언
    ~Person() { cout << "소멸자가 호출되었습니다." << endl; }
    void ShowPersonInfo();
};

int main(void)
{
    unique_ptr<Person> hong = make_unique<Person>("길동", 29);
    hong->ShowPersonInfo();
    return 0;
}

Person::Person(const string& name, int age) // 기초 클래스 생성자의 정의
{
    name_ = name;
    age_ = age;
    cout << "생성자가 호출되었습니다." << endl;
}

void Person::ShowPersonInfo() { cout << name_ << "의 나이는 " << age_ << "살입니다." << endl; }
// 실행 결과
생성자가 호출되었습니다.
길동의 나이는 29살입니다.
소멸자가 호출되었습니다.

일반 포인터와 달리 사용이 끝나고 delete를 사용할 필요가 없습니다.

 


shared_ptr 

shared_ptr은 하나의 특정 객체를 참조하는 스마트 포인터가 총 몇개인지를 참조하는 포인터 입니다.

참조하고 있는 스마트 포인터 개수를 참조횟수(reference count)라고 합니다.

참조 횟수는 특정 객체에 새로운 shared_ptr이 추가될 때 마다 +1 씩 증가하며

수명이 다하였을 때 -1 씩 감소합니다.

따라서 shared_ptr의 수명이 끝나 참조 횟수가 0이 되었을 때 delete 키워드를 사용하여 해제 합니다.

shared_ptr<int> ptr01(new int(5)); // int형 shared_ptr인 ptr01을 선언하고 초기화함.
cout << ptr01.use_count() << endl; // 1
auto ptr02(ptr01);                 // 복사 생성자를 이용한 초기화
cout << ptr01.use_count() << endl; // 2
auto ptr03 = ptr01;                // 대입을 통한 초기화
cout << ptr01.use_count() << endl; // 3

위의 예제에서 사용된 use_count() 멤버 함수는 shared_ptr 객체가 현재 가리키고 있는 리소스를 참조중인 총 소유자 수를 반환해 줍니다.

즉, 처음 생성한 ptr01은 선언초기화때 +1, 복사생성초기화에사용되어 +1, 대입에사용되어 +1 총 3 카운트

 

unique_ptr 때 처럼 make_shared() 함수를 사용하여 shared_ptr 인스턴스를 안전하게 생성할 수 있습니다.

make_shared() 함수는 전달받은 인수를 사용하여 지정된 타입의 객체를 생성하고,

생성된 객체를 가리키는 shared_ptr을 반환하여 줍니다.

#include <iostream>
#include <memory> // 스마트 포인터 사용일 위한 헤더
using namespace std;

class Person
{
private:
    string name_;
    int age_;
public:
    Person(const string& name, int age); // 기초 클래스 생성자의 선언
    ~Person() { cout << "소멸자가 호출되었습니다." << endl; }
    void ShowPersonInfo();
};

int main(void)
{
    shared_ptr<Person> hong = make_shared<Person>("길동", 29);
    cout << "현재 소유자 수 : " << hong.use_count() << endl; // 1
    auto han = hong;
    cout << "현재 소유자 수 : " << hong.use_count() << endl; // 2
    han.reset(); // shared_ptr인 han을 해제함.
    cout << "현재 소유자 수 : " << hong.use_count() << endl; // 1
}

Person::Person(const string& name, int age) // 기초 클래스 생성자의 정의
{
    name_ = name;
    age_ = age;
    cout << "생성자가 호출되었습니다." << endl;
}

void Person::ShowPersonInfo() { cout << name_ << "의 나이는 " << age_ << "살입니다." << endl; }
//실행 결과
생성자가 호출되었습니다.
현재 소유자 수 : 1
현재 소유자 수 : 2
현재 소유자 수 : 1
소멸자가 호출되었습니다.

weak_ptr 

weak_ptr은 하나 이상의 shared_ptr 인스턴스가 소유하는 객체에 대한 접근을 제공하지만,

소유자의 수에 포함되지 않는 포인터 입니다.

shared_ptr은 참조횟수에 따라 동작하는 스마트 포인터 인데

만일 서로가 상대방을 가리키고 있다면 참조횟수는 절대 0이 될 수 없고

0이 되지않는다면 소멸자가 호출되지않아 delete가 실행되지 않는 경우가 생길 수 있습니다.

이런 경우를 순환참조(circular reference)라고 합니다.

때문에 이런 경우를 방지하기위하여 weak_ptr이 필요 합니다.

 

weak_ptr 자세한 내용 참조 사이트

 

[c++] weak_ptr

이번장에서는 weak_ptr에 대해서 알아 보도록 합니다. shared_ptr를 구현하면서 참조 카운트에 영향을 받지 않는 스마트 포인터가 필요했는데 weak_ptr을 사용하면 shared_ptr가 관리하는 자원(메모리)을

jungwoong.tistory.com

 

 

스마트 포인터 전체 내용 참조 사이트

 

코딩교육 티씨피스쿨

4차산업혁명, 코딩교육, 소프트웨어교육, 코딩기초, SW코딩, 기초코딩부터 자바 파이썬 등

tcpschool.com

 

댓글