본문 바로가기

프로그래밍/Modern C++

[C++] 포인터(Pointer)를 컨테이너(Container)에 저장하기

 

 

 

서론

보통은 컨테이너에 객체보단 포인터를 저장하는 것이 더 좋고, 원시 포인터(Raw Pointer)보단 스마트 포인터(Smart Pointer)가 대부분 더 좋다. 포인터를 컨테이너에 저장하는 이유는 다음과 같다.

  • 컨테이너(Container)에 포인터(Pointer)를 저장한다는 것은 포인터가 가리키는 객체가 아니라 포인터가 복사된다는 뜻이다. 포인터 복사가 객체 복사보다 보통은 훨씬 더 빠르다.
  • 컨테이너에 포인터를 저장하면 다형성(Polymorphic behaviour)을 얻을 수 있다. 기반 타입의 원소를 가리키는 포인터를 저장할 수 있게 컨테이너를 정의하면 파생 타입 객체의 포인터도 저장할 수 있다. 공통 기반 클래스가 같은 다양한 객체의 순차 열을 다룰 때 매우 유용한다.  Ex) 선, 곡선 기하학 도형 같은 객체를 처리할 때
  • 포인터를 저장한 컨테이너의 내용을 정렬하는 것이 객체를 정렬하는 것보다 빠르다. 객체가 아니라 포인터만 이동하면 되기 때문이다.
  • 스마트 포인터를 저장하는 것이 원시 포인터를 저장하는 것보다 안전하다. 이는 스마트 포인터를 더는 참조하지 않을 때 메모리(Heap) 영역에 저장된 객체가 자동으로 삭제되기 때문이다. 이렇게 하면 메모리 릭(Memory Leak)이 발생할 확률을 줄일 수 있다. 어떤 것도 가리키지 않는 스마트 포인터는 디폴트 값이 nullptr이다.

스마트 포인터(Smart Pointer)의 종류

스마트 포인터에는 두 가지 기본 타입, unique_ptr <T>와 shared_ptr <T>가 있다. unique_ptr는 가리키는 대상에 대해 독점적인 소유권을 갖지만 shared_ptr는 여러 포인터가 같은 객체를 가리킬 수 있다.

 

원소들을 복사해야 한다면 shared_ptr 객체를 사용해야 하며 복사가 필요 없다면 unique_ptr을 사용하는 것이 좋다.

 

벡터(Vector)에 포인터 저장하기

◈ 원시 포인터(Raw Pointer)를 사용해서 저장
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include<iostream>
#include<vector>
class Player {
private:
    std::string name_;
    unsigned char level_;
public:
    Player() :name_(), level_() {};
    Player(std::string name, unsigned char level) :name_(name), level_(level) {}
    ~Player() { std::cout << "call by Destructor \n"; }
    void DisPlayerUserInfo()const {
        std::cout << "name: " << name_ << " Level: " << static_cast<int>(level_) << std::endl;
    }
};
 
int main() {
    std::vector<Player*>playerList;
    playerList.emplace_back(new Player{ "Park",5 });
    playerList.emplace_back(new Player{ "Kim",15 });
    playerList.emplace_back(new Player{ "Kang",27 });
    playerList.emplace_back(new Player{ "Song",69 });
 
    for (auto& i : playerList)
        i->DisPlayerUserInfo();
    
    for (auto& i : playerList) {
        delete i;
    }
    playerList.clear();
}
 
 
 

결과

원시 포인터(Raw Pointer)는 메모리 릭(Memory Leak)이 발생하지 않도록 주의해야 한다.

 

◈ 스마트 포인터(Smart Pointer)를 사용해서 저장
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include<iostream>
#include<vector>
#include<memory>
 
class Player;
using PPlayer = std::unique_ptr<Player>;
 
class Player {
private:
    std::string name_;
    unsigned char level_;
public:
    Player() :name_(), level_() {};
    Player(std::string name, unsigned char level) :name_(name), level_(level) {}
    ~Player() { std::cout << "call by Destructor \n"; }
    void DisPlayerUserInfo()const {
        std::cout << "name: " << name_ << " Level: " << static_cast<int>(level_) << std::endl;
    }
 
};
 
int main() {
    std::vector<PPlayer>playerList;
    playerList.emplace_back(new Player{ "Park",5 });
    playerList.emplace_back(new Player{ "Kim",15 });
    playerList.emplace_back(new Player{ "Kang",27 });
    playerList.emplace_back(new Player{ "Song",69 });
 
    for (auto& i : playerList)
        i->DisPlayerUserInfo();
 
    playerList.clear();
}
 
 

결과

스마트 포인터(Smart Pointer)는 별도로 메모리 해제를 안 해도 소멸자가 생성된다.

우선순위 큐(Priority_Queue)에 포인터 저장하기

원시 포인터(Raw Pointer)와 스마트 포인터(Smart Pointer)의 차이는 원시 포인터의 메모리 해제의 책임은 개발자에게 있다는 점을 제외하면 본질적으로는 같다.

우선순위 큐(Priority_Queue)나 힙을 생성할 때 원소들의 순서를 결정하는 정렬 순서가 없어서는 안 된다. 포인터를 컨테이너에 저장할 때 에는 사용할 비교 함수를 반드시 제공해야 한다. 비교 함수가 제공되지 않으면 포인터가 가리키는 객체가 아닌 포인터끼리 비교 연산을 진행하게 되는데 이는 대부분 프로그래머가 의도한 동작이 아닐 것이다.

1
auto cmp = [](const shared_ptr<string>& lhs, const shared_ptr<string>& rhs) {return *lhs > *rhs};
 

두 스마트 포인터가 가리키는 객체를 비교하는 람다 표현식을 comp로 정의했다. 람다 표현식에 이름을 붙인 이유는 람다 표현식을 우선순위 큐 템플릿의 타입 인수로 지정하기 위한 것이다.

1
priority_queue <shared_ptr<string>,vector<shared_ptr<string>>, decltype(cmp)> words{ cmp };

첫 번째 템플릿 타입 인수는 저장할 원소의 타입 두 번째 원소는 저장할 컨테이너 타입 세 번째 원소는 비교할 함수 객체의 타입을 지정한 것이다. 람다 표현식의 기본 비교 타입 std::less <T>와 다르기 때문에 세 번째 템플릿 타입 인수를 지정해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include<iostream>
#include<queue>
 
using std::string;
using std::shared_ptr;
using std::unique_ptr;
 
int main() {
    auto cmp = [](const shared_ptr<string>& lhs, const shared_ptr<string>& rhs) {return *lhs > *rhs; };
    std::priority_queue <shared_ptr<string>std::vector<shared_ptr<string>>, decltype(cmp)> words{ cmp };
 
    words.emplace(new string"G" });
    words.emplace(new string"Z" });
    words.emplace(new string"D" });
    words.emplace(new string"C" });
    words.emplace(new string"A" });
    words.emplace(new string"B" });
    words.emplace(new string"I" });
    words.emplace(new string"Y" });
    words.emplace(new string"H" });
 
    while (!words.empty()) {
        std::cout << *words.top() << std::endl;
        words.pop();
    }
}
 
 
 
 

 

결과

 

결론

  • 컨테이너에 포인터를 저장할 수 있다. 이렇게 저장하는 것이 더 효율적이다.
  • 스마트 포인터를 사용하면 필요 없을 때 힙영역에서 객체가 삭제되는 걸 보장한다.
  • 컨테이너에 포인터를 저장한다면 반드시 알고리즘에 필요한 연산이나 비교 연산을 함수 객체로 제공해야 한다.

 

출처 및 레퍼런스

Book: C++14 STL철저 입문 아이버 호튼, 조현태

 

바꾼 용어

원문 바꾼 용어
자유 공간 메모리(heap) 영역
메모리 누수 메모리 릭(Memory Leak)