2년 6개월 동안 일했던 게임회사를 그만두고, 잠깐 재충전시간을 가지면서 미루고 미루었던 STL을 손대게 되었습니다.

1개월 조금 넘게 공부 해보니 겨우 간단한 개념 정도를 다른 사람에게 설명할 정도가 되는군요.(자기자랑? 퍽~)

STL 튜토리얼레퍼런스 가이드 제2판을 중심으로 김학규 사장님이 추천하신 책인 The C++ programming과

이팩티브 STL, game programming gems(1.4게임 프로그래밍에서의 STL)을 참고서적으로 진행계획을 잡았습니다.

스터디를 해본 결과 내용 책 난이도(숫자가 높을수록 어려움)는 아래와 같았습니다.


game programming gems(1.4게임 프로그래밍에서의 STL) - 1

STL 튜토리얼레퍼런스 가이드 제2판 - 2

The C++ programming(사전형식으로 참고함) - 3

이팩티브 STL(시간 관계상 아직 손은 못됨) - 4


이 글의 목적은 STL을 처음 시작하시는 분들을 위해 내용을 정리한 것입니다.

그리고 글의 내용은 game programming gems(1.4게임 프로그래밍에서의 STL)의 내용을 인용한 것을 밝혀둡니다.

예제 코드의 컴파일 환경은 os-xp, 비주얼스트디오 6.0(서비스팩4), STLport(4.6.2)를 사용하였습니다.


게임 프로그래밍에서의 STL 활용 (James Boer)

1.STL의 개요
C++이 공식적으로 표준화된 것은 1997년의 일입니다.
이 때 C++언어 사양이 공식적으로 정의되었을 뿐만 아니라 표준 라이브러리라는 형태로 대규모의 새로운 도구들이 생겨나게 되었는데,
이 라이브러리에서 커다란 부분을 차지하고 잇는 것이 표준 템플릿 라이브러리(STL)이였습니다.

STL의 장점은 속도를 고려해서 설계되었다는 것입니다.
벡터의 범위 점검을 전혀 수행하지 않으며, 반복자는 컨테이너에 접근하기 전에 어떠한 유효성 점검도 수행하지 않는 것이 그 예입니다.

2.STL의 구성 요소들과 용어들
2.1 컨테이너
STL 컨테이너란 벡터, 리스트, 데크, 맵, 셋, 멀티맵, 멀티셋 같은 데이터 추상과 자료 구조들을 말합니다.
컨테이너는 다시 2종류로 나뉘어 불립니다.
벡터, 리스트, 데크는 데이터를 삽입하는 순서가 데이터의 저장 순서에 영향을 미치기 때문에 "순차 컨테이너"라 하고,
맵, 셋, 멀티맵, 멀티셋 같이 삽입된 데이터가 특정한 기준에 의해서 정렬되기 때문에 "정렬 컨테이너"라 합니다.

2.2 반복자 (iterator)
반복자는 컨테이너 안의 요소들에 대한 포인터라고 생각하면 됩니다.
실제로 STL은 컨테이너 데이터의 접근과 탐색에 대한 포인터 방식의 표기법을 사용합니다.
예를 들어서 ++연산자는 반복자를 컨테이너 안의 다음 요소로 옮기는데, 이는 배열 안의 요소를 가리키는 포인터와 동일한 방식입니다.
또 컨테이터 안의 실제 데이터는 *연산자로 반복자를 역 참조함으로서 얻을 수 있는데, 이 역시 일반 포인터와 동일한 방식입니다.

2.3 알고리즘
STL 알고리즘들은 컨테이너 클래스의 맴버 함수형태가 아닌 반복자에 대한 작동하는 동립형 함수들의 형태로 존재합니다.
각 컨테이너들은 비슷한 종류의 반복자를 가지며, 각 컨테이너에 대해 단 한번만 작성되면 됩니다.

3.STL의 주요 개념
STL을 다루는 데 꼭 필요한 몇 가지 기본적인 개념들을 살펴보겠습니다.
모든 컨테이너들은 begin()과 end()라는 메서드들을 제공하는데, 이들을 통해서 컨테이너의 시작과 끝을 알 수 있습니다.
begin()은 컨테이너의 첫 번째 요소를 돌려줍니다. 그러나 end()는 마지막의 유효한 요소 다음 위치를 돌려준다는 점을 유의해야 합니다.

컨테이너
----  ----  ----  ----  ====
-  -  -  -  -  -  -  -  =  =
----  ----  ----  ----  ====
^                       ^
-begin()                -end()

end()가 이렇게 동작하는 이유는 첫 번째로, 빈 리스트에 대한 점검 코드를 생략할 수 있기 때문입니다.
두 번째로, 컨테이너의 요소를 차례로 탐색하는 루프를 만들기 쉽기 때문입니다.(end()에 도달하는 즉시 루프를 끝내면 됩니다).

end()의 위치(마지막의 유효한 요소 다음 위치를 가리키므로)를 가리키는 반복자를 역 참조해서는 안 된다는 점도 기억해야 합니다.

STL 컨테이너들은 정보를 참조가(by ref) 아니라 값(by val)으로 전달합니다.
작은 크기의 데이터 형이라면 값을 그대로 전달해도 문제가 없지만 크기가 큰 구조체나 클래스라면 구조체나 객체 자체 보다는
그들에 대한 포인터를 넘겨주도록 하는 것이 바람직합니다.
값을 그대로 넘겨줄 경우 매 번의 삽입이나 접근 때마다 객체의 복사를 위해 생성자가 호출되는 상황이 벌어지게 되기 때문입니다.

3.1 순차 컨테이너 - 벡터(vector)
STL의 벡터는 동적으로 크기를 변경할 수 있는 배열입니다.(동적 배열)
벡터는 표준 C배열과 거의 동일하게 작동합니다.
메모리를 재할당하면 현재 벡터 안의 요소를 가리키는 반복자들이 무효화되므로
메모리 재할당이 언제 일어나는지 이해하는 것이 중요합니다.(reserve()와 resize())
버퍼 용량이 넘치면 현재 할당된 메모리의 두 배를 할당합니다.

#include <vector>
#include <iostream>

using namespace std;

//코드의 입력과 가독성을 돕기 위한 typedef들
typedef vector<int> IntVector;
typedef IntVector::iterator IntVectItor;

void main()
{
        //정수들을 담는 벡터를 생성한다.
        IntVector c;
        
        //4개의 정수들을 담을 공간을 할당한다.
        c.reserve(4);

        //벡터에 4개의 정수들을 채운다.
        c.push_back(3);
        c.push_back(99);
        c.push_back(42);
        c.push_back(40);

        cout << "예약된 벡터의 크기 =  " << c.capacity() << endl;
        
        //루프로 모든 요소값들을 출력한다.
        for(IntVectItor itor = c.begin(); itor != c.end(); ++itor )
        {
                cout << "element value = " << (*itor) << " ("<< itor << ") "<< endl;
        }

        c.reserve(10);
        cout << "예약된 벡터의 크기 =  " << c.capacity() << endl;
        
        c.push_back(43);
        
        for(itor = c.begin(); itor != c.end(); ++itor )
        {
                cout << "element value = " << (*itor) << " ("<< itor << ") "<< endl;
        }

        c.resize(10);
        c.push_back(100);
        cout << "예약된 벡터의 크기 =  " << c.capacity() << endl;
        for(itor = c.begin(); itor != c.end(); ++itor )
        {
                cout << "element value = " << (*itor) << " ("<< itor << ") "<< endl;
        }

        c[0] = 12;
        c[1] = 32;
        c[2] = 999;
        for(int i = 0; i < c.size(); i++)
                cout << "" << c[i] << " ("<< &c[i] << ")"<< endl;
}

이 예제는 STL 컨테이너를 사용하고자 할 때 필요한 대부분의 기본적인 원칙들을 잘 보여주고 있습니다.
main() 함수 전의 컨테이너와 반복자에 대한 typedef는 자주 사용되는 기법입니다.

//코드의 입력과 가독성을 돕기 위한 typedef들
typedef vector<int> IntVector; // 벡터 컨테이너
typedef IntVector::iterator IntVectItor; // 벡터 컨테이너의 반복자

벡터 컨테이너는 객체c를 생성하고 4개의 정수들을 담을만한 메모리를 준비하는 reserve()함수를 호출합니다.
(reserve()함수는 벡터 컨테이너에서만 사용됩니다.)
벡터 안의 버퍼를 명시적으로 준비하는 데 쓰이는 것은 reserve() 함수입니다.
현재 버퍼 크기를 capacity() 함수로 알아낼 수 있습니다.
만일 capacity()=reserce()이면 벡터에 새 요소를 추가하며 메모리 재할당이 일어나며 현재의 모든 반복자들이 무효화됩니다.
그 다음 push_back()를 이용하여 벡터 컨네이터안에 4개의 정수를 넣습니다.
이 과정에서 미리 4개의 공간을 준비하였으므로 추가적인 메모리 재할당은 일어나지 않습니다.
push_front(), push_back(), pop_front(), pop_back() 함수들은 순차 컨테이너들(벡터, 리스트, 데크)에서 자주 쓰이는 함수들입니다.
벡터의 경우 push_front(),pop_front()의 사용은 피하는 것이 좋습니다.

벡터 컨테이너 안의 데이터를 출력하기 위해 for문에서 반복자를 카운터로 사용하였습니다.
반복자의 초기위치는 begin()으로 설정되며, 매 번의 반복마다 ++연산자에 의해서 그 값이 1씩 증가합니다.
반복자가 end()와 같아지면 루프가 종료됩니다.
반복자는 벡터 안의 현재 위치를 가리킬 뿐, 현재 위치에 있는 요소의 값을 의미하지 않습니다.
따라서 요소의 값을 얻으려면 벡터로부터 요소의 값을 뽑아 내야 합니다.
반복자는 포인터와 동일한 표기법을 사용하며, 실제로 코드에서는 *연산자를 이용해서 요소의 값을 역 참조하고 있습니다.
마지막 for문에서 벡터는 마치 보통의 배열처럼 다룹니다. 그러나 배열 방식의 첨자 표기([])로는 벡터에서 기존 요소 값을 참조하거나
기존 요소 값을 변경하는 것만 가능할 뿐 새 요소를 삽입하는 것은 불가능하다는 점을 주의하기 바랍니다.

3.2 순차 컨테이너 - 리스트(list)
STL에서 가장 널리 쓰이는 것은 아마도 리스트일 것입니다. 리스트는 이중 연결 리스트로 구현되어있습니다.
따라서 어떠한 요소 삽입이나 삭제도 상수적 시간으로 수행됩니다.
대신 벡터나 데크에서처럼 특정 요소에 임의로 접근하는 것이 불가능합니다.
(요소들을 순차적으로 거쳐가야 원하는 요소에 도달할 수 있습니다.)

STL 컨테이너의 한 가지 장점은 명명 규약이나 메서드 사용법이 매우 일관적이라는 사실입니다.
한 종류의 컨테이너를 다루는 방법만 알면 나머지 것들을 다루는 방법도 자동적으로 알게 되는 셈입니다.

#include <list>
#include <iostream>

using namespace std;

class Foo
{
        public:
                Foo(int i)                
                {
                        m_iData = i;
                        m_addriData = &m_iData;
                }
                void SetData(int i) {m_iData = i;}
                int GetData()        const {return m_iData;}
                int * GetAddr() const
                {
                        return m_addriData;
                }
                
        private:
                int m_iData;
                int * m_addriData;
};

// 코드의 입력과 가독성을 돕기 위한 typedef들
typedef list<Foo* > FooList;
typedef FooList::iterator FooListItor;

void main()
{
        //객체(정수를 담는다.)들을 담는 리스트를 생성한다.
        FooList c;
        
        //리스트에 세 개의 요소들을 추가한다.
        c.push_back(new Foo(1));
        c.push_back(new Foo(2));
        c.push_back(new Foo(3));

        int i = 0;

        //루프를 돌린다.
        for(FooListItor itor = c.begin(); itor != c.end();)
        {
                cout << i <<" 번째 요소의 값 = " << (*itor)->GetData() << " ("<< (*itor)->GetAddr() << ")"<< endl;

                if((*itor)->GetData() == 2)
                //리스트 중간의 한 요소를 제거하는 한가지 방법
                {
                        cout << "요소값이" << (*itor)->GetData() << "이면 삭제한다." << " ("<< (*itor)->GetAddr() << ")"<< endl;
                        delete(*itor);
                        itor = c.erase(itor);
                }
                else
                {
                        ++itor;
                        i++;
                }
        }
        
        //2가 삭제된 리스트요소 출력
        cout << "n" << "요소값 2가 삭제되어 있는 확인" << endl;
        for(itor = c.begin(), i=0; itor != c.end(); ++itor,i++)
        {
                cout << i << "번째 요소 = "  << (*itor)->GetData() << " ("<< (*itor)->GetAddr() << ")"<< endl;
        }

        //push_front 이용하여 요소값1 앞쪽에 요소값-1을 입력
        cout << "n" << "push_front 이용하여 요소값1 앞쪽에 요소값-1을 입력" << endl;
        c.push_front(new Foo(-1));
        for(itor = c.begin(), i=0; itor != c.end(); ++itor,i++)
        {
                cout << i << "번째 요소 = "  << (*itor)->GetData() << " ("<< (*itor)->GetAddr() << ")"<< endl;
        }

        //insert를 이용하여 요소값1 뒤에 요소값 2를 다시 입력한다.
        cout << "n" << "insert를 이용하여 요소값1 뒤에 요소값 2를 다시 입력한다." << endl;
        for(itor = c.begin(), i=0; itor !=c.end(); ++itor)
        {
                if( (*itor)->GetData() == 1)
                {
                        itor++;
                        //요소값 2를 입력
                        c.insert(itor,new Foo(2));
                }
        }
        for(itor = c.begin(), i=0; itor != c.end(); ++itor,i++)
        {
                cout << i << "번째 요소 = "  << (*itor)->GetData() << " ("<< (*itor)->GetAddr() << ")"<< endl;
        }

        cout << "n" << "현재 리스트 요소의 개수는? " << c.size() << endl;
        cout << "n" << "리스트가 가질 수 있는 최대 요소의 개수는? " << c.max_size() << endl;

        //모든 요소 값을 지우고 다시 push_back한다.
        cout << "n" << "모든 요소 값을 지우고 다시 push_back한다." << endl;
        for(itor = c.begin(); itor != c.end();)
        {
                delete(*itor);
                itor = c.erase(itor);
        }
        c.push_back(new Foo(10));
        c.push_back(new Foo(20));
        c.push_back(new Foo(30));
        c.push_back(new Foo(40));
        for(itor = c.begin(), i=0; itor != c.end(); ++itor,i++)
        {
                cout << i << "번째 요소 = "  << (*itor)->GetData() << " ("<< (*itor)->GetAddr() << ")"<< endl;
        }
        for(itor = c.begin(), i=0; itor != c.end(); ++itor)
        {
                delete(*itor);
        }
}
위의 리스트 예제는 내장 데이터형이 아닌 사용자 정의 객체들을 담는데, 실제 프로그래밍이라면 이런 쪽이 더 현실적일 것입니다.
class foo는 정수 한 개를 관리하는 객체입니다.
리스트 컨테이너에 요소를 입력하는 방법은 벡터와 마찬가지로 push_back()을 사용하는 방법과,
insert()함수를 사용하는 방법이 있습니다.
리스트를 루프로 돌리면서 요소를 제거할 때에는 주의를 기울여야 합니다.
요소를 제거하면 현재 그 요소를 가리키고 있는 반복자가 무효화되므로 erase() 함수를 적절히 사용해서 반복자가 유효한 요소를
가리키도록 해야 합니다.
erase() 함수는 리턴 값으로 다음의 유효한 위치를 돌려주므로 for문 자체에서 반복자를 증가시키면 결과적으로 반복자가
두 번 증가하는 상황이 됩니다.
그러므로 for문 자체에서 반복자를 증가시키지 말고, 루프 내부에서 요소를 제거하지 않는 경우에만 반복자를 명시적으로
증가시켜 주어야 합니다.

3.3 순차 컨테이너 - 데크
데크는 컨테이너의 양끝에서 요소의 삽입과 제거가 일어나야 하며, 컨테이너의 중간에서는 요소의 삽입이나 제거가 일어날
필요가 없을 때(자주 일어나지 않을 때) 쓰이는 컨테이너입니다.

#include <deque>
#include <iostream>

using namespace std;

typedef deque<int> IntDeque;
typedef IntDeque::reverse_iterator IntDequeRItor;

void main()
{
        //정수들을 담는 데크를 생성한다.
        IntDeque c;

        //데크의 양쪽에 각각 세 개의 요소들을 삽입한다.
        c.push_front(3);
        c.push_front(2);
        c.push_front(1);
        c.push_back(3);
        c.push_back(2);
        c.push_back(1);

        //데크를 "반대 방향"으로 반복한다 - 이를 위해서는
        //특별한 반복자와 함수들이 필요하다.
        for(IntDequeRItor ritor = c.rbegin();ritor != c.rend();++ritor)
        {
                cout << "Value = " << (*ritor) << endl;
        }

        //첫 번째와 마지막 요소를 제거한다.
        c.pop_front();
        c.pop_back();

        //데크의 양끝의 요소들을 참조한다.
        //데크가 비어 있는지 점검하는 것이 필요하다.
        //존재하지 않는 요소를 참조하려 하면 정의되지 않은 행동이 일어나게 된다.
        //보통은 접근 위반 에러가 발생한다.
        if(!c.empty())
        {
                cout << "Front = " << c.front() << endl;
                cout << "Back = " << c.back() << endl;
        }        
}
벡터나 리스트의 예제들과 비슷한 코드이나, 몇 가지 다른 점이 있습니다.
후진 반복자(reverse iterator)이 그것입니다.
후진 반복자가 따로 존재하는 이유는 itor != begin()식으로 역방향 루프의 종료 조건을 판단 할 수 없기 때문입니다.
역방향 루프를 위해서는 rbegin(), rend()를 사용해야 합니다.
rbegin()는 마지막 유효 요소 값을 가리키고, rend()는 첫 번째 유효한 이전의 위치를 가리킵니다.
후진 반복자는 반복자가 증가할수록 컨테이너의 앞쪽으로 나아갑니다.
이번 예제에는 pop_front(), pop_back()이 등장하는데, 이들은 각각 컨테이너의 앞과 뒤에서 요소 값들을 제거합니다.
또 front(), back()이라는 함수들도 처음 소개되었는데, 이들은 리턴 값으로 처음과 마지막 요소 값들을 넘겨줍니다.
리턴 값으로 요소 값을 넘겨주는 처리를 하기 전에는 empty() 함수를 이용하여 컨테이너가 비어 있지 않은가를 점검해야 합니다.

3.4 정렬 컨테이너 - 맵
맵들(map, multimap)은 STL의 기본 컨테이너들 중에서 아마 가장 복잡하고 가장 다목적인 컨테이너(가장 많이 쓰인다고 함)
일 것입니다.
여기서는 가장 기본적인 map에 대해서만 얘기할 것입니다.
나머지(multimap, set, multiset) 정렬 컨테이너는 각자 공부해 보시기 바랍니다.

map에는 임의의 두 종류의 데이터들이 키-값 쌍의 형태로 저장됩니다.
키를 통해서 값을 조회하는데 걸리는 시간은 O(log n)인데, 해시 테이블보다 비효율적이지만 속도 차이는 무시할 정도이며,
삽입과 함께 정렬이 수행된다는 추가적인 장점이 존재합니다.
다시 말하면 map의 데이터는 항상 정렬된 상태로 존재합니다.

#pragma warring(disable:4786)
#include <map>
#include <iostream>
#include <string>
#include <algorithm>

using namespace std;

//코드의 입력과 가독성을 돕기 위한 typedef들
typedef map<int, string> isMap;
typedef isMap::value_type isValType;
typedef isMap::iterator isMapItor;

void main()
{
        isMap c;
        
        // 키-값 쌍들을 삽입
        c.insert(isValType(100, "One Hundred"));
        c.insert(isValType(3, "Three"));
        c.insert(isValType(150,"One Hundred Fifty"));
        c.insert(isValType(99, "Ninety Nine"));

        // 모든 키들과 값들을 출력
        for(isMapItor itor = c.begin(); itor != c.end(); ++itor)
        {
                cout << "Key = " << (*itor).first << " , Value = " << (*itor).second << endl;
        }
        cout << endl;

        //맵을 연관 배열처럼 다룰 수도 있다.
        cout << "맵을 연관 배열처럼 다룰 수 있다." << endl;
        cout << "Key 3 displays value " << c[3].c_str() << endl;
        cout << endl;

        // 다음과 같은 방식으로 키-값 쌍을 삽입하는 것도 가능하다.
        cout<< "c[123] = One Hundred Twenty Three 이러한 방법으로 삽입하는 방법" << endl;
        c[123] = "One Hundred Twenty Three";
        for(itor = c.begin(); itor != c.end(); ++itor)
        {
                cout << "Key = " << (*itor).first << " , Value = " << (*itor).second << endl;
        }
        cout << endl;

        cout << "키 값이 123인 특정한 요소를 제거한다." << endl;
        // 키에 기반해서 특정한 요소를 찾고 제거한다.
        isMapItor pos = c.find(123);
        if(pos != c.end())
                //요소를 삭제하면 그것을 가리키는 반복자는 무효화된다.
                //그 상태에서 그냥 pos++를 호출하면 정의되지 않은 행동이
                // 발생할 것이다.
                c.erase(pos);
        for(itor = c.begin(); itor != c.end(); ++itor)
        {
                cout << "Key = " << (*itor).first << " , Value = " << (*itor).second << endl;
        }
        cout << endl;

        //루프 도중에 요소를 제거하는 경우에는 이런 식으로
        cout << "루프 도중에 키 값이 3인 요소를 제거한다." << endl;
        for(isMapItor itr = c.begin(); itr != c.end(); )
        {
                if(itr->second == "Three")
                        c.erase(itr++);
                else
                        ++itr;
        }
        for(itor = c.begin(); itor != c.end(); ++itor)
        {
                cout << "Key = " << (*itor).first << " , Value = " << (*itor).second << endl;
        }
}

value_type라는 새로운 데이터 형은 컨테이너 안에 키-값 쌍의 형태로 저장되는 요소들을 나타내기 위한 것입니다.
typedef를 이용해서 이 데이터 형과 다른 데이터 형들을 좀더 쓰기 쉬운 형태로 정의했습니다.
요소(키-값 쌍)를 컨테이너에 삽입할 때에는 insert()함수를 이용합니다.
다른 컨테이너와의 차이점은 map::value_type 형의 값을 넣는다는 점입니다.
map는 요소가 삽입될 때 자동적으로 키에 의해 정렬된 상태를 유지합니다.
map는 두 개의 항목(키-값)을 쌍으로 저장하는 컨테이너이므로 하나의 반복자로 두 개의 항목들에 접근하는 것이 가능하도록 설계되었습니다.
이것이 바로 value_type 구조체입니다.
반복자를 역 참조하면 value_type 구조체를 얻게 됩니다. value_type에는 first와 second라는 두 개의 데이터 멤버들이 있는데,
first는 키를 담고 있으며 second는 값을 담고 있습니다.
map는 키 값을 통한 임의 접근이 가능한데, 이 경우 map은 연관 배열과 동일한 방식으로 동작합니다.
코드 후반부에 find() 함수로 특정 키에 해당하는 요소를 찾아 지우는 구문이 있습니다.
직접 루프를 돌려서 요소를 제거 할 수도 있습니다.
여기서 주의할 점은 다른 컨테이너와 달리 map의 erase() 함수가 다음의 유효한 위치를 돌려주도록 구현하지 않았습니다.
이 때문에 다른 컨테이너들과는 다른 방식으로 요소들을 제거해야 합니다.
반복자를 for루프 안에서 조건에 따라 증가시키는 방법이 그것 입니다.
요소를 제거해야 하는 경우에는 후행 증가(반복자를 참조한 후 증가)를 사용했고,
요소를 제거하지 않는 경우에는 선행증가를 사용하였습니다.
선행증가와 후행증가가 나와서 잠깐 설명을 하려고 합니다.
루프 안에서는 항상 선행증가를 사용하는 것이 효율적인 측면에서 보면 좋습니다.
후행 증가 연산자는 변수의 이전 값을 돌려주므로 이전 값을 담을 임시적인 변수를 만들고 파괴하는 과정이 일어나기 때문입니다.
특별한 사유가 아니라면 항상 선행 증가를 사용하는 것이 좋습니다.

4. 정리후기
3일정도면 끝날 줄 알았는데 정리하고 어색한 부분을 고치고 하다 보니 10일이나 걸리는 군요.
역시 혼자 이해 하고 있는 것과 이해 하고 있는 것을 다른 사람이 보기 편하게 정리 하는 것의 차이가 크다는 것을 다시 한번 느낍니다.
서두에서 이야기한 내용이지만 STL을 처음 공부하시려는 분들께 도움이 되었으면 합니다.
그럼 고수가 되는 그날을 위해서...

p.s. 그라나도 에스파다 정말 재미있게 하고 있습니다. 플레이 해본 결과 imc games의 입사를 심각하게 고민하고 있습니다.
     그리고 아이보리서버에서 하시는 분 중 게임 같이 하실분 있으시면 래이크 가문으로 귓말 주세요.    

- 여담 -
리스트에 관련해 공부하던 중 다른 게임회사에 다니는 룸메이트(대학동기)와 논쟁이 있었습니다.
위의 리스트예제에서 객체를 생성할 때 왜 new를 사용했냐는 것이죠.
친구의 주장은 어차피 반복자가 포인터 역할을 해주기 때문에 벡터(동적 배열)나 리스트(이중링크드리스트) 같은 컨테이너 안에 실제 객체를 넣고,
반복자를 이용해서 연산 처리를 하면 된다는 것이었습니다.
이 논쟁 덕분에 되짚어 보게 된 "포인터"와 "참조" 그리고 "실제 객체를 왜 new로 선언하였는지"를 정리해 볼까 합니다.

포인터를 설명하기에 앞서서 기초가 되는 메모리를 간단히 설명하겠습니다.
메모리는 총5개의 영역으로 나뉘어져 있습니다.

첫 번째로 전역 명칭 영역이 입니다.
전역 명칭 영역은 전역 변수들이 저장되어 있는 곳입니다.

두 번째로 스택이 있습니다.
스택에는 지역 변수와 함수 매개 변수들이 있는 곳입니다.

세 번째로 레지스터가 있습니다.
명령 포인터와 스택의 상단 위치 기록 등의 내부 관리 기능을 하고 있는 곳입니다.

네 번째로 코드영역이 있습니다.
말 그대로 소스 코드의 각 행들이 일련의 명령어로 바뀌어 있는 곳입니다.

다섯 번째로 자유 저장 영역(힙)이 있습니다.
1,2,3,4 영역을 제외한 모든 메모리로 제일 큰 크기를 자랑합니다.

포인터를 사용한다는 것은 가장 큰 메모리 영역을 사용할 수 있다는 얘기입니다.

메모리 얘기는 대충 정리하고 포인터가 있어야 하는 이유를 설명해 보겠습니다.
포인터가 있어야 하는 이유는 지역변수와 전역변수의 문제점을 보완하기 위해서입니다.
지역 변수의 문제는 그 변수가 지속되지 않는다는 것입니다.(함수가 리턴될 때 스택에서 폐기됩니다.)
전역 변수는 프로그램 전체를 통해서 무제한 접근할 수 있지만
프로그램이 비대해지면 이해하고 관리하기 어려운 코드가 생길 수 있습니다.
이러한 문제점들을 자유 저장 영역에 데이터를 둠으로써 해결할 수 있게 한 것입니다.

자 이제 포인터를 사용하는 경우를 알아보겠습니다.
포인터를 사용하는 경우는 아래의 3가지 경우 입니다.
첫 번째로 "자유 저장 영역(힙)에 있는 데이터 관리"(new,delete),
두 번째로 "클래스 멤버 데이터와 함수의 액세스"(->연산자),
세 번째로 "참조를 통해 변수를 함수에 전달"이 있습니다.

세 번째 내용이 오늘의 주제로 "왜 new로 선언하였는지?"을 알아봅시다.

객체를 값으로 함수에 전달할 때, 객체의 복사본이 만들어집니다.
그리고 함수로부터 객체를 값으로 반환 받을 때마다 또 다른 복사본이 만들어집니다.
복사본이 만들어진다는 얘기는 복사 생성자와 소멸자가 호출된다는 이야기입니다.
큰 객체의 생성자와 소멸자의 호출은 속도를 떨어트리고, 메모리의 부담을 가중시킵니다.
역으로 말하면 생성자와 소멸자의 호출을 최소화하는 것이 좋다는 것입니다.

예제 소스를 보면서 살펴 보겠습니다.

#include <list>
#include <iostream>

using namespace std;

//아주 큰 객체라고 가정한다.
class Foo
{
        public:
                Foo(int i, int nT)                
                {
                        m_iData = i;
                        m_addriData = &m_iData;
                        m_nType = nT;
                        cout << "생성자" << endl;
                }
                Foo(const Foo & rhs)
                {
                        m_iData = rhs.GetData();
                        m_addriData = rhs.GetAddr();
                        m_nType = rhs.GetType();
                        cout << "복사생성자" << endl;
                }
                ~Foo()
                {
                        cout << "소멸자" << endl;
                }
                void SetData(int i) {m_iData = i;}
                int GetData()        const {return m_iData;}
                int * GetAddr() const {return m_addriData;}
                int GetType() const {return m_nType;}
                
        private:
                int m_iData;
                int * m_addriData;
                int m_nType;
};
// 코드의 입력과 가독성을 돕기 위한 typedef들
typedef list<Foo*> FooListPtr;
typedef FooListPtr::iterator FooListItorPtr;
typedef FooListPtr::pointer FooListptrPtr;

typedef list<Foo> FooList;
typedef FooList::iterator FooListItor;
typedef FooList::pointer FooListptr;
void main()
{
        FooList c;
        FooListItor Inititor;

        Foo Foo1(10,0);
        c.push_back(Foo1); // 여기에서 함수에 실제 객체를 넘긴다.
        for(FooListItor itor = c.begin(); itor != c.end();)
        {
                itor = c.erase(itor);
        }
}
소스의 내용은 간단합니다 정수를 관리하는 객체를 선언하고 1개의 객체를 리스트 컨퍼넌트에 집어넣었습니다.
그리고 다시 지우는 프로그램입니다.

결과
생성자
복사생성자
소멸자
소멸자

main()함수의 내용을 살짝 수정하여 new로 선언해 보겠습니다.
void main()
{
        FooListPtr cPtr;
        FooListItorPtr InititorPtr;

        Foo *ptrFoo = new Foo(100,0);
        cPtr.push_back(ptrFoo); // 함수에 포인터를 넘긴다.
        for(FooListItorPtr itorPtr = cPtr.begin(); itorPtr != cPtr.end();)
        {
                delete (*itorPtr);
                itorPtr = cPtr.erase(itorPtr);
        }
}

결과
생성자
소멸자

위의 소스와 아래소스는 하는 일은 같지만 위의 소스의 경우
push_back()함수에 객체를 값으로 함수에 전달하기 때문에 복사 생성자와 소멸자가 호출됩니다.
반면 아래 소스에는 포인터로 전달 하기 때문에 생성자와 소멸자의 호출이 없습니다.
이것이 new로 선언한 이유입니다.
(new와 delete에 대한 생성자와 소멸자만 존재함)

좀더 나아가 객체나 변수를 함수에 전달하는 방법을 알아보겠습니다.
객체나 변수를 함수에 전달하는 방법은 3가지 방법이 있습니다.
첫 번째는 실제 객체를 처리하는 방법입니다.
두 번째는 포인터를 이용한 처리하는 방법입니다.
세 번째는 참조를 이용한 처리하는 방법입니다.

아래의 예제는 리스트의 예제를 약간 변형 시켜서 객체를 함수에 전달하는 구분을 추가해보았습니다.

값에 의한 처리(call by value)
Foo Damage2(Foo thisFoo, int nDagamgvalue) // 객체가 함수에 전달될 때 복사생성자와 소멸자가 호출됩니다.
{
         thisFoo.SetData(thisFoo.GetData() - nDagamgvalue);
         return thisFoo; //객체가 리턴될 때도 복사 생성자와 소멸자가 호출됩니다.
}
void main()
{
        FooList c;

        //리스트에 세 개의 요소들을 추가한다.
        c.push_back(Foo(10,0));
        c.push_back(Foo(20,1));
        c.push_back(Foo(30,0));

        //모든 객체에 -15를 해준다.
        for(FooListItor itor = c.begin(); itor != c.end(); ++itor)
        {
                cout << (*itor).GetData() << " ("<< (*itor).GetAddr() << ")"<< endl;

                if((*itor).GetType() == 1)
                {
                        // 리스트의 값 수정
                        (*itor) = Damage2((*itor),15);
                }
        }
        cout << "Damage2() 함수 사용 - 객체을 직접 전달"<< endl;
        for(itor = c.begin(); itor != c.end(); ++itor)
        {
                cout << (*itor).GetData() << " ("<< (*itor).GetAddr() << ")"<< endl;
        }
        cout << "객체를 직접 전달하였기 때문에 리턴 값이 있어야만 값이 변한다." << endl;
        for(itor = c.begin(); itor != c.end();)
        {
                itor = c.erase(itor);
        }
        cout << endl;
}

결과

생성자
복사생성자
소멸자
생성자
복사생성자
소멸자
생성자
복사생성자
소멸자
10 (0x0012ff4c)
20 (0x0012ff40)
복사생성자
복사생성자
소멸자
소멸자
30 (0x0012ff34)
Damage2() 함수 사용 - 값을 직접 전달
10 (0x0012ff4c)
5 (0x0012ff40)
30 (0x0012ff34)
소멸자
소멸자
소멸자

주소에 의한 처리(call by address)
void DamagePtr3(Foo *thisFoo, int nDagamgvalue)
{
         thisFoo->SetData(thisFoo->GetData() - nDagamgvalue);
}
void main()
{
        FooListPtr cPtr;

        cPtr.push_back(new Foo(100,0));
        cPtr.push_back(new Foo(200,1));
        cPtr.push_back(new Foo(300,0));

        //모든 객체에 -15를 해준다.
        for(FooListItorPtr itorPtr = cPtr.begin(); itorPtr != cPtr.end(); ++itorPtr)
        {
                cout << (*itorPtr)->GetData() << " ("<< (*itorPtr)->GetAddr() << ")"<< endl;

                if((*itorPtr)->GetType() == 1)
                {
                        // 리스트의 값 수정
                         DamagePtr3((*itorPtr),15);
                }
        }
        cout << "DamagePtr3() 함수 사용 - 객체를 new로 선언 후 포인터 사용"<< endl;
        for(itorPtr = cPtr.begin(); itorPtr != cPtr.end(); ++itorPtr)
        {
                cout << (*itorPtr)->GetData() << " ("<< (*itorPtr)->GetAddr() << ")"<< endl;
        }
        cout << "포인터 연산으로 리턴 값이 없어도 값이 변한다." << endl;

        for(itorPtr = cPtr.begin(); itorPtr != cPtr.end();)
        {
                delete (*itorPtr);
                itorPtr = cPtr.erase(itorPtr);
                
        }
}
결과

생성자
생성자
생성자
100 (0x00393dd8)
200 (0x00393e10)
300 (0x00393e48)
DamagePtr3() 함수 사용 - 객체를 new로 선언 후 포인터 사용
100 (0x00393dd8)
185 (0x00393e10)
300 (0x00393e48)
포인터 연산으로 리턴 값이 없어도 값이 변한다.
소멸자
소멸자
소멸자

void DamagePtr3(Foo *thisFoo, int nDagamgvalue)
{
         thisFoo->SetData(thisFoo->GetData() - nDagamgvalue);
}
void main()
{
        FooList c;

        //리스트에 세 개의 요소들을 추가한다.
        c.push_back(Foo(10,0));
        c.push_back(Foo(20,1));
        c.push_back(Foo(30,0));

        //모든 객체에 -15를 해준다.
        for(FooListItor itor = c.begin(); itor != c.end(); ++itor)
        {
                cout << (*itor).GetData() << " ("<< (*itor).GetAddr() << ")"<< endl;

                if((*itor).GetType() == 1)
                {
                        // 리스트의 값 수정
                        DamagePtr3(&(*itor),15);
                }
        }
        cout << "DamagePtr3() 함수 사용 - 객체를 직접 선언한 후 포인터를 사용"<< endl;
        for(itor = c.begin(); itor != c.end(); ++itor)
        {
                cout << (*itor).GetData() << " ("<< (*itor).GetAddr() << ")"<< endl;
        }
        cout << "참조를 사용하여 함수에 전달하는 방법과 같은 결과가 나온다." << endl;
        cout << "참조를 사용하는 방법보다 코드 가독성이 떨어진다." << endl;
        for(itor = c.begin(); itor != c.end();)
        {
                itor = c.erase(itor);
        }
        cout << endl;
}
결과

생성자
복사생성자
소멸자
생성자
복사생성자
소멸자
생성자
복사생성자
소멸자
10 (0x0012ff4c)
20 (0x0012ff40)
30 (0x0012ff34)
DamagePtr3() 함수 사용 - 객체를 직접 선언한 후 포인터를 사용
10 (0x0012ff4c)
5 (0x0012ff40)
30 (0x0012ff34)
참조를 사용하여 함수에 전달하는 방법과 같은 결과가 나온다.
참조를 사용하는 방법보다 코드 가독성이 떨어진다.
소멸자
소멸자
소멸자

참조에 의한 처리(call by reference)
void DamageRef5(Foo &thisFoo, int nDagamgvalue)
{
         thisFoo.SetData(thisFoo.GetData() - nDagamgvalue);
}
void main()
{
        FooList c;

        //리스트에 세 개의 요소들을 추가한다.
        c.push_back(Foo(10,0));
        c.push_back(Foo(20,1));
        c.push_back(Foo(30,0));

        //객체 타입이 1인 객체의 데이터에 -15를 해준다.