3.1.2 vector에 순차적으로 보관

작성할 프로그램은 장르 관리 프로그램입니다. 장르에는 장르 번호와 장르명이 있습니다. 장르 번호는 순차적으로 부여하며 장르명은 사용자에게 입력받습니다. 장르 번호와 장르명으로 삭제할 수 있고 검색할 수 있습니다. 그리고 모든 장르 목록을 확인할 수 있습니다.

제공할 기능을 살펴보면 장르 추가, 장르 번호로 장르 삭제, 장르명으로 장르 삭제, 장르 번호로 검색, 장르명으로 검색, 모든 장르 목록 확인이 있어요.

vector에 순차적으로 보관할 때는 push_back 메서드를 사용하세요. 원하는 자료를 찾을 때는 반복자를 이용하여 찾거나 find, find_if 알고리즘을 이용하여 찾습니다. 삭제할 때는 자료가 있는 위치를 찾아 erase 메서드를 사용하세요. 모든 자료를 탐색할 때는 반복자를 이용하세요.

이 책에서는 프로그래밍을 통해 vector를 사용하는 방법을 소개할 것입니다. 따라서 vector에서 제공하는 다양한 멤버들 중에 작성할 프로그램에 필요한 멤버들만 소개하는 한계를 갖습니다. 대신 실제 프로그래밍에 자주 사용하는 것에 초점을 맞추기 때문에 실질적인 노하우를 터득할 수 있는 장점을 갖습니다.

먼저 장르를 구현한 후에 vector를 이용하여 구현하기로 할게요.

class Genre
{
    //장르 클래스에는 장르 번호와 장르명을 멤버 필드로 선언하세요.
    const int num;
    string name;
public:
    //생성자는 장르 번호와 장르명을 입력 인자로 받습니다.
    Genre(int num,string name);
    //장르 번호와 장르 이름을 확인할 수 있게 접근자를 제공하세요.
    int GetNum()const;
    string GetName()const;
    //장르 정보를 콘솔 화면에 출력하는 메서드도 제공합시다.
    void View()const;
};

생성자에서는 입력 인자로 멤버 필드를 설정하세요.

Genre::Genre(int num,string name):num(num)
{
    this->name = name;
}

접근자 메서드에서는 장르 번호와 장르명을 반환하세요.

int Genre::GetNum()const
{
    return num;
}
string Genre::GetName()const
{
    return name;
}

콘솔 화면에 장르 정보를 출력하는 메서드도 정의하세요.

void Genre::View()const
{
    cout<<"장르 No."<<num<<" 장르명:"<<name<<endl;
}

이제 장르를 관리할 형식을 정의합시다. 장르를 관리할 클래스를 App라 정할게요. vector를 사용하기 위해 vector 파일을 포함하세요.

#include <vector>

find와 find_if 알고리즘을 사용하기 위해 algorithm 파일을 포함하세요.

#include <algorithm>

사용할 이름을 using 문으로 작성하세요. using namespace std;로 간략하게 할 수 있지만 앞으로 우리는 STL에서 제공하는 이름과 같은 이름으로 만드는 실습도 할 거예요. 그 때 이름 충돌이 나지 않게 하기 위해 명확하게 사용할 이름을 using 문으로 작성하세요.

using std::vector;
using std::find;
using std::find_if;

동적으로 생성한 장르를 보관할 vector를 Genres 이름으로 타입 재지정합시다. 타입 재지정하면 사용할 때 간략하게 표현할 수 있어 사용하기 편합니다.

typedef vector<Genre *> Genres;

반복자는 STL의 자료구조마다 내부에 iterator 이름으로 정의하고 있습니다. 이를 GIter 이름으로 타입 재지정하세요.

typedef Genres::iterator GIter;

상수화 메서드에서 반복자를 사용할 수 있게 const_iterator도 제공하고 있어요. 마찬가지로 GCIter 이름으로 타입 재지정하세요.

typedef Genres::const_iterator GCIter;

class App
{
    //장르를 보관할 컬렉션을 멤버 필드로 추가하세요. 
    //배열, 연결리스트 등의 자료구조 형식의 개체를 컬렉션이라고 부릅니다. 
    Genres genres;
    //그리고 가장 최근에 부여한 장르 번호도 멤버 필드로 필요하겠죠.
    int last_gnum;//가장 최근에 부여한 장르 번호

public:
    App(void);
    ~App(void);
    void Run();
private:
    //메뉴를 출력하고 사용자가 입력한 메뉴를 반환하는 메서드를 제공하세요.
    int SelectMenu();
    //그리고 제공해야 할 각 기능에 관한 메서드를 제공해야겠죠.
    void AddGenre(); //장르 추가
    void RemoveGenreByNum();//번호로 장르 삭제
    void RemoveGenreByName();//이름으로 장르 삭제
    void FindGenreByNum()const; //번호로 장르 검색
    void FindGenreByName()const; //이름으로 장르 검색
    void ListGenre()const; //장르 목록 보기
};

이제 App 클래스의 메서드들을 구현합시다.

App::App(void)
{
    //생성자에서는 가장 최근에 부여한 장르 번호를 초기화를 해야겠죠.
    last_gnum = 0;
}

void App::Run()
{
    int key=NO_DEFINED;
    //사용자가 종료 메뉴를 선택하지 않으면 반복합니다.
    while((key = SelectMenu())!=ESC)
    {
        //사용자가 입력한 키에 따라 약속한 기능을 호출하게 하세요.
        switch(key)
        {
        case F1: AddGenre(); break;
        case F2: RemoveGenreByNum(); break;
        case F3: RemoveGenreByName(); break;
        case F4: FindGenreByNum(); break;
        case F5: FindGenreByName(); break;
        case F6: ListGenre(); break;
        default: cout<<"잘못 선택하셨습니다."<<endl; break;
        }
        //수행한 작업을 확인할 수 있게 키를 입력해야 다시 메뉴 선택할 수 있게 하세요.
        cout<<"아무 키나 누르세요."<<endl;
        ehglobal::getkey();
    }
}

메뉴를 출력하고 입력한 키를 반환하는 메서드를 구현하세요.

int App::SelectMenu()
{
    ehglobal::clrscr();//콘솔 화면을 지우기
    cout<<"장르 관리 프로그램 [ESC]: 종료"<<endl;
    cout<<"F1: 장르 추가 F2: 번호로 장르 삭제 F3: 이름으로 장르 삭제"<<endl;
    cout<<"F4: 번호로 장르 검색 F5: 이름으로 장르 검색 F6: 장르 목록 보기"<<endl;
    return ehglobal::getkey();//사용자가 입력한 기능 키 반환
}

장르를 추가하는 메서드를 구현합시다.

void App::AddGenre() //장르 추가
{
    //먼저 부여할 장르번호를 위해 last_gnum을 1 증가해야겠죠.
    last_gnum++;
    //장르명을 입력받으세요.
    cout<<last_gnum<<"번째 추가할 장르명:";
    string name = ehglobal::getstr();//장르명 입력
    //장르를 생성하세요.
    Genre *genre = new Genre(last_gnum,name); //장르 생성
    //벡터에 순차 보관할 때는 push_back 메서드를 사용합니다. 
    //내부적으로 동작하는 원리는 벡터 만들기에서 소개하기로 할게요.
    genres.push_back(genre);//순차 보관
}

테스트를 쉽게 하기 위해 장르 목록 보기 기능을 먼저 구현하기로 해요.

void App::ListGenre()const //장르 목록 보기
{
    //vector에는 자료를 보관한 시작 위치를 반환하는 begin 메서드가 있습니다. 
    //ListGenre 메서드가 상수화 메서드이므로 const_iterator 형식을 재지정한 
    //GCIter 형식 변수를 선언한 것입니다.
    GCIter seek = genres.begin(); //보관한 시작 위치
    //vector에는 자료를 보관한 마지막 다음 위치를 반환하는 end 메서드가 있습니다. 
    //end 메서드가 반환하는 위치는 마지막 자료를 보관한 위치 다음 위치입니다. 
    //주의하셔야 할 부분이죠.
    GCIter last = genres.end(); //보관한 마지막(다음) 위치
    const Genre *genre;
    //반복자는 비교 연산과 증감 연산자를 제공하고 있습니다.
    for(   ;seek!=last; ++seek)
    {
        //반복자는 간접 연산을 제공하고 있으며 연산 결과는 보관한 자료입니다.
        genre = (*seek); //간접 연산으로 seek위치에 보관한 장르 확인
        genre->View(); //장르 정보 출력
    }
}

이번에는 번호로 장르 삭제 기능을 구현합시다. 장르 목록 보기 기능처럼 반복자를 이용하여 원하는 번호의 장르의 위치를 구할 수도 있습니다. 여기에서는 다른 방법을 사용할게요.

STL에서는 find와 find_if 알고리즘을 제공하여 원하는 위치를 찾을 수 있습니다. find 알고리즘은 검색할 구간(구간의 시작과 끝)과 검색할 자료를 입력 인자로 전달하면 처음 발견한 위치를 반환합니다. 그리고 find_if 알고리즘은 검색할 구간과 함수 호출이 가능한 표현(함수명 혹은 함수 호출 연산자를 정의한 개체)을 입력 인자로 전달하면 처음으로 함수 호출 연산의 결과가 참인 위치를 반환합니다.

find_if에 전달할 함수의 원형은 입력 인자로 보관한 형식이 오고 반환 형식이 참과 거짓인지 판별할 수 있어야 합니다. 함수 개체를 전달한다면 해당 개체도 입력 인자로 보관한 형식이 오고 반환 형식이 참과 거짓인지 판별할 수 있는 함수 호출 연산을 정의하고 있어야 합니다.

STL의 find_if를 따라가 보면 다음과 같은 _Find_if를 호출하는 부분까지 도달합니다.

template<class _InIt, class _Pr> inline
_InIt _Find_if(_InIt _First, _InIt _Last, _Pr _Pred)
{
    for (; _First != _Last; ++_First)
        if (_Pred(*_First))
        break;
    return (_First);
}

템플릿 인자를 두 개 정의하고 있죠. 입력 인자로 받은 두 개의 형식은 같고 세 번째 인자는 다른 것을 알 수 있습니다. 그리고 for문에서 두 개의 인자를 비교하여 다를 때까지 반복하고 있으며 첫 번째 입력 인자를 받은 변수를 증가하는 것을 알 수 있어요.

그리고 세 번째 입력 인자로 받은 형식에 입력 인자로 첫 번째 입력 매개 변수에 간접 연산을 하고 있으며 결과가 참일 때 반복문을 탈출하여 첫 번째 입력 매개 변수를 반환하고 있어요.

find_if 알고리즘에서는 검색 구간(첫 번째 인자는 검색 구간의 시작, 두 번째 인자는 검색 구간의 마지막)과 함수(혹은 함수 개체)를 인자로 받아 구간 내에 보관한 자료를 함수에 적용했을 때 참인 부분을 만나면 반복문을 탈출하여 해당 위치를 반환하고 있음을 알 수 있어요.

이와 같은 정보만 가지고 STL을 사용하는 것은 어느 정도 노하우를 갖고 있지 않다면 어려운 일입니다. 다행히 find나 find_if 알고리즘을 사용하지 않아도 반복자를 이용하여 원하는 구현을 할 수 있죠. 하지만 이와 같은 표현을 사용할 수 있다면 앞으로 프로그래밍할 때 이러한 매커니즘으로 만들어진 라이브러리를 사용할 수 있기 때문에 도전해 볼만한 가치가 있어요.

실제 이와 같은 매커니즘은 이벤트 처리하는 곳에서는 매우 흔한 작업입니다. 여러분께서 이와 같은 부분을 캐치하지 못하더라도 라이브러리 사용을 자주하면서 익힐 수도 있을 거예요.

다음은 번호로 보관한 장르를 찾고자 할 때 정의한 클래스입니다.

class EqualerByNum
{
    //검색할 번호를 멤버 필드로 선언하세요.
    int num;
public:
    //생성자에게 검색할 번호를 입력 인자로 받아 멤버 필드에 선언하세요.
    EqualerByNum(int num)
    {
        this->num=num;
    }
    //함수 호출 연산자에는 보관한 형식이 와야 하고 반환 형식은 참과 거짓인지 판별할 수 있어야 합니다.
    bool operator()(const Genre *genre)const
    {
        //연산 내부는 검색할 번호와 보관한 장르의 번호가 같은지 여부를 반환하겠죠.
        return num == genre->GetNum();
    }
};

번호로 장르 삭제 기능을 구현합시다.

void App::RemoveGenreByNum()//번호로 장르 삭제
{
    //먼저 삭제할 장르 번호를 입력 받으세요.
    int num=0;
    cout<<"삭제할 장르 번호:";
    num = ehglobal::getnum();
    //입력한 번호로 함수 개체를 만드세요.
    EqualerByNum ebnum(num);
    //find_if 알고리즘에 장르를 보관한 벡터의 시작과 끝을 구간으로 전달하고 
    //검색할 번호를 멤버로 갖는 함수 개체를 전달하세요.
    GIter seek = find_if(genres.begin(), genres.end(), ebnum);
    //find_if 알고리즘에서는 원하는 조건의 자료를 찾지 못하면 
    //두 번째 인자로 전달한 구간의 끝을 반환합니다. 
    //따라서 find_if에 전달할 두 번째 인자는 검색 대상이 아닙니다. 
    //물론 앞에서 얘기했듯이 end 메서드가 반환하는 것은 
    //마지막 자료를 보관한 다음 위치이므로 당연히 검색 대상이 아니겠죠.
    if(seek != genres.end())
    {
        //주의할 점은 vector에서 제거하는 것은 보관한 자료를 지운 것이지 
        //해당 개체를 메모리에서 소멸하는 것은 아닙니다. 
        //언제나 자신이 생성한 개체는 자신이 소멸하세요. 
        //간접 연산으로 보관한 개체를 얻어 소멸하세요.
        Genre *genre = (*seek);
        delete genre;
        genres.erase(seek);
        cout<<"삭제하였습니다."<<endl;
    }
    else
    {
        cout<<"존재하지 않는 장르 번호입니다."<<endl;
    }
}

같은 방법으로 장르명으로 도서 삭제 기능을 구현합시다. 먼저 검색할 이름으로 보관한 장르를 찾을 때 사용할 클래스를 정의해야겠죠.

class EqualerByName
{
    //검색할 장르명을 멤버 필드로 선언하세요.
    string name;
public:
    //생성자에서는 검색할 장르명을 입력받아 멤버 필드에 설정해야겠죠.
    EqualerByName(string name)
    {
        this->name = name;
    }
    //함수 호출 연산자를 정의하여 멤버 필드의 장르명과 같은 장르인지 판별해야겠죠.
    bool operator()(const Genre *genre)const
    {
        return name.compare(genre->GetName())==0;
    }
};

이름으로 장르 삭제하는 기능을 구현합시다.

void App::RemoveGenreByName()//이름으로 장르 삭제
{
    string name;
    //먼저 삭제할 장르명을 입력받아야겠죠.
    cout<<"삭제할 장르명:";
    name = ehglobal::getstr();
    //삭제할 장르명을 입력 인자로 함수 개체를 생성하세요.
    EqualerByName ebname(name);
    //find_if 알고리즘으로 삭제할 장르가 있는 위치를 검색해야겠죠.
    GIter seek = find_if(genres.begin(), genres.end(), ebname);
    //나머지 부분은 장르 번호로 검색과 같습니다.
    if(seek != genres.end())
    {
        Genre *genre = (*seek);
        delete genre;
        genres.erase(seek);
        cout<<"삭제하였습니다."<<endl;
    }
    else
    {
        cout<<"존재하지 않는 장르명입니다."<<endl;
    }
}

이제 번호로 장르 검색 기능을 구현합시다.

void App::FindGenreByNum()const //번호로 장르 검색
{
    int num=0;
    //검색할 장르 번호를 입력받으세요.
    cout<<"검색할 장르 번호:";
    num = ehglobal::getnum();
    //검색할 장르 번호를 입력 인자로 함수 개체를 생성하세요.
    EqualerByNum ebnum(num);
    //find_if 알고리즘으로 검색할 장르를 보관한 위치를 검색하세요.
    GCIter seek = find_if(genres.begin(), genres.end(), ebnum);

    //만약 반환한 위치가 구간의 끝이면 검색할 장르를 보관하지 않는 것입니다.
    if(seek != genres.end())
    {
        //검색한 위치의 반복자의 간접 연산으로 보관한 장르를 구하세요.
        Genre *genre = (*seek);
        //검색한 장르 정보를 출력하세요.
        genre->View();
    }
    else
    {
        cout<<"존재하지 않는 장르 번호입니다."<<endl;
    }
}

이름으로 장르 검색 기능도 구현하세요.

void App::FindGenreByName()const //이름으로 장르 검색
{
    string name;
    //먼저 검색할 장르명을 입력받아야겠죠.
    cout<<"검색할 장르명:";
    name = ehglobal::getstr();
    //마찬가지로 입력한 장르명으로 함수 개체를 생성하세요.
    EqualerByName ebname(name);
    //find_if 알고리즘으로 검색할 장르를 보관한 위치를 검색하세요.
    GCIter seek = find_if(genres.begin(), genres.end(), ebname);
    if(seek != genres.end())
    {
        //검색한 장르가 있을 때 반복자의 간접 연산으로 보관한 장르를 구하세요.
        Genre *genre = (*seek);
        //그리고 장르의 정보를 출력하세요.
        genre->View();
    }
    else
    {
        cout<<"존재하지 않는 장르명입니다."<<endl;
    }
}

소멸자에서는 자신이 생성한 장르 개체를 모두 소멸해야겠죠.

App::~App(void)
{
    //장르를 보관한 시작 위치와 마지막(다음) 우치를 구하세요.
    GIter seek = genres.begin(); //보관한 시작 위치
    GIter last = genres.end(); //보관한 마지막(다음) 위치
    Genre *genre;
    for(   ;seek!=last; ++seek)
    {
        //반복자에 간접 연산으로 보관한 장르를 구하여 소멸하세요.
        genre = (*seek); //간접 연산으로 seek위치에 보관한 장르 확인
        delete genre;
    }
}
int main()
{
App 개체를 생성한 후에 Run 메서드를 호출하여 상호 작용한 후에 소멸하는 것으로 끝입니다.
    App *app = new App();
    app->Run();
    delete app;
    return 0;
}

STL을 사용하는 것이 처음인 분들은 낯설 수 있을 거예요. 하지만 STL에서 제공하는 대부분의 자료구조는 사용 방법이 비슷하며 점차 노하우가 생길 거예요. 프로그래밍 언어 문법을 학습하거나 자료구조, 알고리즘을 학습하는 것과 다르게 제공하는 라이브러리는 자주 사용하여 사용 방법에 익숙해 지는 것이라 할 수 있습니다. 물론 이 책에서는 알고리즘과 자료구조를 설명하는 책이며 STL 사용법을 소개하는 이유는 우리가 작성할 자료구조의 모델이기 때문입니다. 물론 STL 사용법을 익히는 것 자체도 의미있는 일임에 틀림없구요.