Joinc 팀블로그 리눅스 메뉴얼 정리 Joinc 위키
댓글

Recent Comments

Powered by Disqus
팀블로그 카테고리
  전체 (1105)
   공지사항 (1)
   검색엔진 (21)
   기술동향 (58)
   게임 (2)
   독서 (6)
   리눅스 (12)
   보안 (1)
   사회문제 (22)
   어셈블리 (43)
   영화 (3)
   오픈소스 (10)
   음악 (9)
   인물 (1)
   포인터 (4)
   프로그래머 (23)
   팀블로그 (20)
   테터툴즈 (29)
   C/C++ (152)
   FireFox (11)
   Gimp (2)
   Google (98)
   Java (13)
   Perl (2)
   Pthread (11)
   STL (13)
   TCP/IP (8)
   Tools (31)
   Web2.0 (42)
   Wiki (1)
«   2010/09   »
      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    

'STL'에 해당되는 글 4건

  1. 2007/06/08 STL 을 이용한 DOS 공격검사 프로그램 (6)
  2. 2007/05/29 STL - Association 컨테이너 (4)
  3. 2007/05/22 STL - Sequences 컨테이너 (5)
  4. 2007/05/14 STL 소개 (7)
2007/06/08 22:10

STL 을 이용한 DOS 공격검사 프로그램

이 문서는 수정될 수 있습니다. 최신 문서는 Joinc Wiki 에서 확인하세요.

Iterator

윤 상배

dreamyun@yahoo.co.kr



1절. 소개

이번글에는 STL 에 대한 간단한 응용을 포함한다. 대량의 데이타를 취급해야 할때 어떻게 작업을 해야 할런지에 대한 몇가지 문제들을 담고 있다.

이를 위해서 DOS ATTACK을 검출하는 어플리케이션을 예로 들어서 설명을 할것이다. 그러나 이 문서는 어디까지나 방법을 제시하는 문서로써 완벽한 코드를 제시하진 않을것이며, 테스트 가능한 수준의 코드만을 제공할것이며 구현에 대한 고민은 각자의 몫이 될것이다.

DOS 공격에 대한 내용은 네트웍 보안의 기본(1)을 참고하기 바란다.


2절. DOS ATTACK 검출 어플리케이션 제작

간단한 DOS 공격을 검색하는 어플리케이션을 만든다고 가정을 해보자. DOS 공격의 경우 보통 하나의 IP에서 특정 포트로 짧은시간에 요청이 들어올것임으로, "5초에 20번의 요청이 있으면 DOS 공격으로 간주한다" 라는 식으로 DOS 공격에 대해서 정의가 가능하다.

그렇다면 어플리케이션은 해당패킷이 들어올경우 IP 정보를 분석해서 카운팅하고, 이 카운팅이 5초 내에 20번 발생 했다면 "DOS WARN 메시지"를 시켜야 할것이다.

이 어플리케이션을 제작하는데 있어서 가장 중요한 포인트는 자료구조의 유지와 효율성이 될것이다. DOS 어택을 판단하는 근거는 IP와 PORT 가 됨으로, 모든 입력 IP와 PORT에 대한 테이블을 유지하고 있어야 한다.

그런데 아시다 시피 IP의 범위는 너무 넓다. 좀 널리 알려진 바쁜서버 라면 짧은시간에 수천-수만 혹은 그이상의 서로다른 IP 에서의 접근이 이루어질수 있을거다. 만약 라우터에 설치할경우 더욱 많은 접근이 일어날것이다. 이걸 단순히 map 이나 vector 로 IP정보 테이블을 구성할경우 효율에 문제가 발생할수 있다.


2.1절. 효율적인 자료구조를 생각해보자

그럼 어떻게 해야 효율적인 자료구조를 만들수 있을까.. 단순한 map 으로 할경우 만약 IP가 수만개가 들어온다면, 수만개의 원소로 이루어진 거대한 map 데이타가 만들어질 것이며, 더 커질수도 있을것이다. 거기에 port 정보까지 포함해야 됨으로, 단순 map 으로 구성하기에는 원소의 갯수가 너무 많아진다.

map 은 정렬연관 컨테이너로 균형 이진트리 구조를 가진다. 다음은 map 의 복잡도(Complexity guarantees) 이다. size() 는 map 에 포함된 원소의 갯수이며, count(k) 는 삭제하고자 하는 key 의 갯수이다. N 은 원소의 갯수이다.

표 1. 평균 복잡도(Average complexity)

erase keyO(log(size()) + count(k))
erase elementconstant time
범위 삭제O(log(size()) + N)
countO(log(size()) + count(k))
findlogarithmic
예를 들어 13만개 정도의 원소를 가진 map 에서 특정한 원소를 찾을경우 평균 걸리는 시간은 약 17 정도가 될것이다. 1,000,000 은 약 20 이다.

DOS 공격을 검색해내는 어플리케이션이라면 라우터에 설치될것도 생각해봐야 한다. 그래서 평균 1,000,000 의 데이타에서 서취하는걸로 해서 계산해보면 대량 20 정도의 시간이 소모된다. 20 은 이처럼 바쁜어플리케이션 꽤나 큰수치이다. 그럼으로 시간을 좀더 줄여야 할 필요성이 있다.

게다가 이 어플리케이션을 위해서 map 을 구성한다고 했을때 map 의 key 는 IP와 PORT 의 조합이 될것임으로, 백만 이상의 데이타를 감당할수 있다고 봐야할것이다.


2.1.1절. 해쉬를 이용한 효율성 높이기

그래서 생각할수 있는게 Hash 함수의 사용이다. Hash 함수는 데이타 축약을 위한 함수로써, 같은 값을 입력했을때는 언제나 같은 데이타 축약을 얻어낼수 있는 함수이다.

우리가 축약해야할 데이타는 물론 32bit 크기의 IP 주소 이다. 32bit 는 범위가 너무나 넓음으로 이것을 1024(2^10) 정도로 줄이도록 하겠다. HASH 함수는 매우 단순무식한 방법으로 IP 주소 32bit 중 10bit 만을 이용해서 Hash 값을 얻도록하는데, 단지 쉬프트 연산을 해주는 것만으로 해결가능하다.

int hash(ulong ip)
{
return ip >> 22;
}
이 값들은 0 부터 1023 까지 분포가 될것임으로 vector 혹은 array 로 자료구조를 구현할수 있을것이다. IP 가 들어오면 0 - 1023 중에 하나의 값으로 hashing 될건데, vector 에 집어넣을때는 이 hashing 된 값자체를 첨자로 해서 입력할수 있음으로 시간복잡도는 O(1) 이 될것이다. 다른 말로 "한키" 에 자신이 저장될 곳을 찾아갈수 있게 된다. 이 vector 는 키를 IP와 PORT로 가지는 map 을 원소로 가지게 될것이다.

가장 이상적인 상황은 IP가 2^20 개가 들어있는데 0에서 1023까지의 vector 에 아주 균등하게 들어있는 상황으로 각 vector 의 원소는 1024 개의 원소를 가지는 map 을 가지게 될것이다. 이상황에서 원하는 데이타 (IP,PORT를 키로 가지는)를 찾는데 걸리는 시간은 "vector 에서 첨자연산하는데 걸리는 시간 O(1)" + "O(log1024)" 가 될것이다. 그럼으로 대략 11 번정도에 원하는 값을 찾을수 있게 된다. 최초 map 만을 사용했을때의 20 에 비하면 거의 절반수준으로 줄어듦을 알수 있다.

물론 위의 상황은 매우 이상적인 상황이며, 실제로는 위의 경우에 비해서 더 많은 시간이 소모될것이다. 왜냐하면 위의 단순한 해쉬함수로는 IP가 균등하게 0-1023 으로 분포되기가 힘들것이기 때문이다. 해쉬함수를 만드는데 있어서 가장 중요한 부분은 바로 입력데이타들에 대해서 최대한 균등하게 분포되는 해쉬값을 얻어내는 것인데, 위의 함수로는 분명히 균등하게 분포되지 않을것이다. 하지만 일단은 위의 해쉬함수를 사용하도록 하겠다. 좀더 성능좋은 해쉬함수는 많은 고민과 테스트를 통해서 만들어 질수 있을것이다.(공개된것들도 많이 있으니 google 등을 통해서 확인해보기 바란다)


2.1.2절. 최종적인 자료구조

다음은 이렇게 해서 만들어진 최종적인 자료구조이다.

그림 1. hash_map 자료구조

hash 값을 위한 1024 크기의 vector 가 놓인다. 이 vector 은 IP,PORT 를 key 로 가지고, TIME, COUNT 를 value로 가지는 map 을 원소로 가진다. TIME 은 패킷이 들어온 시간이며, COUNT 는 몇번 들어왔는지를 계수하기 위해서 사용된다. 이 map 의 자료구조는 대충 다음과 같을 것이다.
typedef struct _key
{
int ip;
int port;
} key;

typedef struct _value
{
int time;
int count;
} value;

struct ltstr
{
bool operator() (key a1, key a2)
{
return memcmp((void *)&a1, (void *)&a2, 8) < 0;
}
};
map<key, value, ltstr> mydata;
ltstr 을 주목하기 바란다. ltstr 은 key 를 정렬하기 위해서 정의되었는데, memcmp 를 이용해서 2개의 구조체의 크기를 비교한다. 2개의 int 를 가지고 있음으로 8byte 크기로 비교하면 될것이다. 다음은 테스트를 위한 간단한 코드이다.

map_test.cc

#include <vector>
#include <map>
#include <iostream>

using namespace std;

// map 의 KEY 값으로 ip와 port 정보를 가진다.
typedef struct _key
{
ulong ip;
int port;
} key;

// map 의 VALUE 값으로 처음 만들어진 시간과
// 카운트 갯수이다.
typedef struct _value
{
int time;
int count;
} value;

// KEY 인 struct key 를 정렬하기 위해서 사용한다.
// memcmp 를 이용해서 정렬하도록 한다.
struct ltstr
{
bool operator() (key a1, key a2)
{
return memcmp((void *)&a1, (void *)&a2, 8) < 0;
}
};

int main()
{
key mkey;
value mvalue;
map<key, value, ltstr> mydata;

mkey.ip = 134;
mkey.port = 4;
mydata[mkey] = mvalue;

mkey.ip = 134;
mkey.port = 3;
mydata[mkey] = mvalue;

mkey.ip = 132;
mkey.port = 1;
mydata[mkey] = mvalue;

map<key,value,ltstr>::iterator mi;
mi = mydata.begin();
while(mi != mydata.end())
{
cout << mi->first.ip << ","<< mi->first.port << endl;
*mi++;
}
}
정렬순서는 먼저 ip를 우선으로 한다. 만약 ip 가 동일할경우 port 번호로 정렬이 될것이다. 아직은 만들어진 map 정보를 hash 테이블에 입력하지 않았는데, 2.1.3절에서 설명할것이다.

2.1.3절. skeleton code (골격 코드)

이제 실제적인 골격 코드를 만들어 보도록 하자. 이건 어디까지나 지금까지 우리가 생각했던 자료구조가 실제 구현될수 있는지를 확인해 보는 테스트수준의 코드이다.

이 어플리케이션은 IP와 Port 번호 를 이용해서 Key 를 만들고 만약 동일한 IP, Port 에서 연결이 들어왔다면, count 를 1씩 증가시킨다. 만약 지정된 시간(위으 코드에서는 30초) 동안 100 번이상의 count 가 발생할경우 Dos 공격이라고 판단하고 워닝 메시지를 출력한다.

#include <vector>
#include <map>
#include <time.h>
#include <iostream>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/types.h>

using namespace std;

typedef struct _key
{
ulong ip;
int port;
} key;

typedef struct _value
{
int time;
int count;
} value;

struct ltstr
{
bool operator() (key a1, key a2)
{
return memcmp((void *)&a1, (void *)&a2, 8) < 0;
}
};
int hash(ulong ip)
{
return ip >> 22;
}

map<key, value, ltstr> mydata;
vector<map<key, value, ltstr> > hash_map(1024);

void InitValue(value *mvalue);
int insert_packet(key akey);

int main()
{
key mkey;
value mvalue;

// 테스트용 자료 입력 ----------------
mkey.ip = inet_addr("192.168.100.1");
mkey.port = 21 ;
insert_packet(mkey);

mkey.ip = inet_addr("192.168.100.1");
mkey.port = 2;
insert_packet(mkey);

mkey.ip = inet_addr("192.168.100.3");
mkey.port = 22;
insert_packet(mkey);

mkey.ip = inet_addr("192.168.100.200");
mkey.port = 80;
insert_packet(mkey);

mkey.ip = inet_addr("192.168.100.1");
mkey.port = 2;
insert_packet(mkey);
// 테스트용 자료 입력 ----------------


// 테스트를 위해서 출력한다.
// 192.168.100.1 에 대한 hash 키를 만들고
// 값을 출력해본다.
mkey.ip = inet_addr("192.168.100.1");
mkey.port = 2;

map<key, value, ltstr>::iterator mi;

cout << hash(mkey.ip) << endl;
mi = hash_map[hash(mkey.ip)].find(mkey);
if (mi == hash_map[hash(mkey.ip)].end())
{
cout << "not found " << endl;
}
else
{
cout << mi->second.time << ":" << mi->second.count << endl;
}
}

// key 를 입력받아서
// hash 키를 만들어서 해당 hash_map 자료구조에 입력한다.
int insert_packet(key akey)
{
value mvalue;
int hash_value = hash(akey.ip);
map<key,value,ltstr>::iterator mi;
mi = hash_map[hash_value].find(akey);

InitValue(&mvalue);

// 만약 이미 존재하고 있다면
// count 를 증가 시킨다.
if (mi != hash_map[hash_value].end())
{
cout << "Count 증가" << endl;
mi->second.count++;
}
// 그렇지 않다면 새로 생성한다.
else
{
cout << "Insert " << endl;
hash_map[hash_value].insert(pair<key, value>(akey, mvalue));
}
}

void InitValue(value *mvalue)
{
mvalue->time = time((time_t *)NULL);
mvalue->count = 1;
}

2.2절. 문제 발생(효율 문제)

비록 골격 코드이긴 하지만 위의 코드가 비교적 효율적으로 작동하리란걸 예상할수 있다. 그러나 이건 어디까지나 데이타 입력의 경우이다.

위의 방식으로 해서 데이타가 계속 쌓이는 건 그렇다 치고, 저런 상태로 몇시간만 돌면 분명히 map 에는 수십/수백만건의 데이타가 쌓이게 될것이다. 기본적으로 카운터가 일정수를 넘어가야만 지워지기 때문에 카운터가 초과했을경우 자료구조에서 지워진다고 하더라도, 그렇지 않은 수많은 데이타는 지워지지 않을것이기 때문이다. 결국 몇시간도 안되어서 모든 시스템 메모리 자원을 몽땅 써버리게 될것이다.

이러한 문제의 해결을 위해서 value 에 time 이라는 변수를 두긴 했음으로, 일정시간 마다 time 를 확인하고, 초과된 메시지는 지워줌으로써 해결이 가능할 것이다.

그렇다면 어떻게 지워줄것인가.. 단순한방법은 vector 의 0번부터 1023번까지의 모든 map 을 순환하면서 일일이 time 비교를 해서 time 이 초과했을경우 지워주는 방법인데, 척 봐도 알겠지만, 너무너무 비효율적이다. 데이타가 십만 혹은 백만 정도 들어있다고 가정해 보자. 제대로 작동하는걸 기대하기 힘들것이다.


2.2.1절. 해결 - 이터레이터 저장하기

이럴경우 생각할수 있는 방법이 garbage 데이타 정리를 위한 별도의 자료구조를 하나 만들어서 유지하는 것이다. 여러가지 자료구조를 생각할수 있는데, 여기에서는 multimap을 사용하도록 했다. key 는 time 이 될것인데, 이 time 이 분명히 겹칠수 있을것이기 때문이다.

multimap<int, map<key.value.ltstr>::iterator> garbage_collect;
아이디어는 간단하다. 위에서 우리가 map 데이타를 입력할때 동시에 위의 garbage_collect 자료구조에 map데이타의 iteraotr 을 입력시켜주는 것이다. 그리고 일정시간이 지나면 garbage_collect 자료구조를 순환하면서 시간이 초과된 iterator 에 대해서 삭제를 해주면 될것이다. 포인터를 별도로 저장해놓고 포인터를 이용해서 garbage 데이타를 삭제해주는 것과 비슷한 방식이다.

시간 복잡도를 계산해보면 garbage_collect 에서 원하는 범위의 원소를 삭제하는데 걸리는 시간 "O(log(size()) + N)", garbage_collect 에서 나온 iterator 를 이용해서 삭제시키는데 걸리는 시간 O(1) + N, ip 를 hash 로 바꾸는데 걸리는 시간 O(1) + N 정도가 된다.

O(log(size()) + N) + O(2N) 
N 은 삭제해야할 원소의 갯수이다. 예를들어 1000000 개의 데이타에서 100 개의 데이타를 삭제해야 한다고 가정했을때, 6+100+200 = 306 정도가 될것이다.

hashvector 의 처음부터 끝까지 순환하는 무식한 방법을 사용했을경우 순환에 걸리는 시간 N 에 비교하는데 걸리는 시간, 삭제하는데 걸리는 시간등을 예상하면 최소 2000000 시간이 소모될것이다. (순환에 걸리는 시간 1000000 + 비교하는데 걸리는시간 1000000, 거기에 삭제할 원소 N개)

그럼 간단하게 테스트 코드를 하나 만들어 보도록 하겠다. 비록 테스트 코드이긴 하지만 "이터레이터 저장" 아이디어가 현실적인지는 확인해 볼수 있을 것이다.

iterator_map.cc

#include <map>
#include <string>
#include <iostream>
using namespace std;

typedef struct _mydata
{
int age;
int num;
} mydata;

int main()
{
mydata md;

// map 자료구조및 iterator 선언
map<int, mydata> data[1024];
map<int, mydata>::iterator mi, my;

// iterator 를 저장하기 위한 map 과
// 이것의 iterator 선언
map<int, map<int,mydata>::iterator> garbage_map;
map<int, map<int,mydata>::iterator>::iterator mmi;

// 테스트용 데이타 입력
md.age = 12;
md.num = 1;
data[0][1] = md;

md.age = 19;
md.num = 3;
data[0][3] = md;

md.age = 28;
md.num = 2;
data[0][2] = md;

md.age = 40;
md.num = 22;
data[0][4] = md;

md.age = 42;
md.num = 26;
data[0][5] = md;

md.age = 38;
md.num = 12;
data[0][6] = md;
// 테스트용 데이타 입력 종료

int i = 1;
mi = data[0].begin();
// 테스트용 데이타를 iterator 로 순환하면서
// 해당 iterator 를 iterator 를 저장하기 위한 map 에
// 입력
while(mi != data[0].end())
{
// 입력 디버깅용 출력
cout << mi->first << ": " << mi->second.num << ","
<< mi->second.age<< endl;
// iterator 를 자료구조에 저장함
garbage_map[i] = mi;
*mi++;
i++;
}
cout << "==============================" << endl;

// key 의 값이 4보다 더큰 이터레이터를 모두 삭제한다.
mmi = garbage_map.find(4);
while(mmi != garbage_map.end())
{
cout << "erase " << endl;
data[0].erase(mmi->second);
*mmi++;
}

// garbage map 에서 필요없는 데이타를 삭제한다.
mmi = garbage_map.find(4);
garbage_map.erase(mmi, garbage_map.end());
cout << "iterator num " << garbage_map.size() << endl;

cout << "==============================" << endl;

// map 데이타를 출력한다. 디버깅용
mi = data[0].begin();
while(mi != data[0].end())
{
cout << mi->first << ": " << mi->second.num << ","
<< mi->second.age<< endl;
*mi++;
}
cout << "==============================" << endl;
}
간단하게 테스트 가능할 거다.

3절. 결론

이상 간단하게 STL에 대한 몇가지 이슈에 대해서 알아보았다. 개인적으로는 DOS ATTACK 검출을 위한 좀더 완벽한 예제를 들어보고 싶었으나 그렇게 하기 위해선 패킷캡쳐와 이를 효율적으로 분석해서 검출해 내는 방법에 대해서 좀더 고민해야 하기 때문에(한마디로 귀찮아서) 완벽한 예제를 만들지는 못했다.

언젠가 이 문서의 후속편이 만들어지면 좀더 완벽한 예제를 포함시키도록 하겠다.


:::
2007/05/29 21:59

STL - Association 컨테이너

글 내용은 수정될 수 있습니다. 최신 내용은 Joinc Wiki에서 확인하세요.

STL(2) - Association 컨테이너

윤 상배

dreamyun@yahoo.co.kr



1절. Association 컨테이너

1.1절. Association 컨테이너란?

우리는 이미 STL(2) - sequence container에서 컨테이너에 대한 개요를 잡으면서 Sequences 컨테이너에 대한 내용을 다루었다. 우선은 Association 컨테이너가 Sequences 컨테이너와 어떠한 면에서 다른지에 대해서 알아보도록 하겠다.

엄밀히 말하자면 Association 컨테이너는 Sorted Associative Container 과 Hash Associative Container 2개의 컨테이너를 포함한다. 그러나 아직 Hash 관련 컨테이너는 표준이 아님으로 이글에서는 Sorted Associative Container 만을 다룰것이다. 이글에서의 Associative Container 은 Sorted Associative Container 를 말한다.

정렬 연관 컨테이너 Sorted Associative Container 라고도 말하는 이유는 Sequence 컨테이너가 순서대로 (마치배열처럼) 컨테이너에 적재되는것과는 달리, Association 컨테이너는 "키 => 값" 의 형태로 컨테이너에 적재때문이며, 이때 "키" 연속적으로 컨테이너에 적재되기 않고, Sort 후에 컨테이너에 적재되기 때문이다. Sort 후에 적재되는 가장 큰이유는 역시 "키" 값을 이용한 검색의 속도를 빠르게 하기 위한 목적이 가장 클것이다.

물론 Sequences 역시 "키 => 값"의 형태로 적재된다는건 Association 컨테이너와 같지만 Sequences 컨테이너는 "키" 값으로 단지 sequence 한 정수로 이루어지며, 정렬이 되지 않고 Sequence 하게 컨테이너에 적재된다는 점이 Association 과는 다르다.

아래의 예는 Association 컨테이너중 가장 대표적인 map 에 대한 간단한 예제이다.

예제 : name_map.cc

include <string>

int main()
{
map<string, string> cfg_data;

cfg_data["goodday"] = "김길동";
cfg_data["yundream"] = "윤드림";
cfg_data["zood"] = "홍무개";
}
위의 예제를 컴파일 해서 실행시켜보자. 그럼 다음과 같은 결과를 보여줄것이다.
[root@localhost test]# ./name_map
goodday : 김길동
yundream : 윤드림
zooz : 홍무개
분명히 최초 컨테이너에 값을 입력할때는 "yundream", "goodday", "zooz" 순서로 넣었는데, 이걸 출력해보면 "goodday", "yundream", "zooz" 으로 정렬되어 있다. 이처럼 map 은 값을 입력할때 키 값을 기준으로 정렬되어서 컨테이너에 적재함으로 나중에 특정 키 값을 이용해서 값을 찾을때 매우 빠른시간에 찾을수 있게 된다.

Association 컨테이너도 다른 STL 컨테이너와 마찬가지로 컨테이너의 크기가 알아서 조절된다. 원소의 삭제/삽입을 지원하지만, Sequence 컨테이너와 같이 특정한 위치에 원소를 삽입할수 있는 기능은 제공하지 않는다(당연하다. sort 후에 컨테이너에 적재되므로 위치지정할 필요가 없다).

이러한 Association 컨테이너에는 map, multimap, set, multiset 등이 있다. 다음장에서는 이러한 각각의 컨테이너에 대해서 알아보도록 하겠다.


2절. 컨테이너 클래스 설명

이번 장에서는 map, multimap, set, multiset 등에 대해서 설명하도록 하겠다.


2.1절. map

2.1.1절. 특징

위에서 말했듯이 map 은 Sorted Associative Container - 정렬 연관 컨테이너 - 이며, Key 와 Value 의 쌍으로 이루어진 자료구조이다. php나 perl, python 과 같은 언어에서 map 은 매우 기본적인 자료구조이며, 이들언어를 사용해 봤다면 ("mon" => "월요일", "Tue => "화요일") 과 같은 문장에 매우 익숙할것이다. map 을 사용하면 Key 에 대한 Value 의 자료형으로 모든것을 사용할수 있다. int, string, char * 형은 물론이며, object, struct, 또다른 컨테이너 들까지 제한없이 사용가능하다.

map 를 사용할때 key 는 겹치면 안된다. 만약 key 가 겹치게 되면, value 의 값이 변경되게 된다. key 는 유일해야 한다.


2.1.2절. 선언

map 은 다음과 같이 선언할수 있다.

표 1. 템플릿 Parameters

Parameter 설명 기본값
Key map 의 key 이다.  
Data Key 가 가르키는 데이타 이다.  
Compare Key 가 정렬되기 위해사용되는 크기 비교함수이다. 첫번째 아규먼트가 2번째 아규먼트보다 작으면 true 를 그렇지 않으면 false 를 리턴하도록 만든다. less<key>
Alloc map의 할당기 이다. map 은 이 할당기를 통해서 메모리 관리를 한다. alloc

보통은 기본할당기를 사용하게 됨으로 다음과 같이 map 을 만들어서 사용할수 있다.

#include <map>
#include <string>

map<string, string> mydata;
mydata["mon"] = "월요일";
mydata["thu"] = "화요일";
cout << mydata["mon"] << endl;

map 의 parameter 중에 3번째를 보면 Compare 가 있다. 이것은 Key 비교를 위한 함수이다. 바로 위의 예제의 경우는 Compare 함수를 정의해서 사용하지 않고 있다. 이유는 string 이 string 객체가 비교가 가능하기 때문이다(int 형도 마찬가지다). 그러나 char * 타입의 경우에는 char * 타입간 비교가 불가능하므로 Compare 함수를 아래와 같이 선언해서 사용해야 할것이다.

struct ltstr
{
bool operator(){const char *s1, const char *s2) const
{
return strcmp(s1, s2) < 0;
}
};

int main()
{
map<char *, char *, ltstr> mydata;
mydata["mon"] = "월요일";
mydata["thu"] = "화요일";
}


2.1.3절. 자주 사용되는 멤버 함수들

기본적으로 STL 에서 사용되는 대부분의 컨테이너들은 같은이름의 함수를 제공한다. 이는 프로그래머로 하여금 쉽게 사용할수 있도록 하기 위함이며, 간단히 컨테이너 이름만 바꿈으로써 전혀 다른 컨테이너로의 전환이 손쉽게 하기 위함이다.


2.1.3.1절. 위치 접근 관련

Associative Container 의 경우 Sequence 컨테이너와 다르게 "첨자" 접근이 불가능 하다. 원래가 첨자를 통해서 원소에 접근하는 방식이 아니기 때문이다. 그러므로 map 의 경우 iterator 나 Key 이름을 통해서 접근해야만 한다.

iterator 의 사용은 다른 STL 컨테이너와 같이 컨테이너의 처음위치를 돌려주는 begin() 과 마지막을 돌려주는 end()가 있다.

iterator begin();
iterator end();
이들은 다음과 같이 사용가능할것이다.
struct ltstr
{
bool operator()(const char *s1, const char *s2) const
{
return strcmp(s1, s2) < 0;
}
};

int main()
{
map<char *, char *, ltstr> mydata;
mydata["january"] = 31;
mydata["february"] = 28;
mydata["march"] = 31;
mydata["april"] =30
mydata["may"] =31;

cout << "april -> " << mydata["april"] << endl;

map<char *, char *, ltstr>::iterator cur = mydata.begin();
while(cur != mydata.end())
{
cout << cur->first << ":" << cur->second << endl;
*cur++;
}

}

find()를 이용하면 일치하는 key 이름을 가르키는 iterator 를 되돌려준다.

iterator find(const key_type& k); 
map<char *, char *, ltstr>::iterator fi;
fi = find("march");
cout << fi->second << endl;


2.1.3.2절. 삽입 / 삭제 관련

원소를 삽입하거나 변경하기 위한 가장 간단한 방법은 "name[key]=value" 을 이용한 방법이다. 동일한 key 이름이 없다면 새로 삽입될것이고, 이미 동일한 key 이름이 있다면 기존의 key 의 value 값이 변경될 것이다.

map 은 또한 원소를 입력하기 위한 좀더 나은 기능을 가진 insert()라는 함수를 제공한다(이함수는 다른 multimap, set, multiset 등에도 사용된다).

pair<iterator, bool> insert(const value_type&& x);
iterator insert(iterator pos, const value_type&& x);
template <class InputIterator> void insert(InputIterator, InputIterator);
insert 를 이용해서 원소를 삽입할경우 x 키가 이미 있다면, 변경되고 그렇지 않다면 삽입된다. iterator 을 줄경우 insert 를 위해서 위치검색을 하게될 위치를 지정해 줄수도 있다.
map<string, string> cfg_data;

cfg_data.insert(pair<string, string>("yundream", "ok"));
cfg_data.insert(pair<string, string>("yundream3", "ok"));
map<string, string>::iterator mi;
mi = cfg_data.begin();
*mi++;
cfg_data.insert(mi, pair<string, string>("yundream2", "ok"));

값을 변경하는 또다른 방법은 iterator->second 를 이용한 방법이다. 원하는 키 값의 위치를 find() 를 통해서 찾아내고 iterator->second = value 의 방식을 이용해서 값을 변경할수 있다.

원하는 key 를 삭제하기 위해서 erase()를 사용하면 된다.

void erase(iterator pos);
size_type erase(const key_type &k);
key 값을 이용해서 삭제할수도 있고, 위치를 가르키는 iterator 를 알고 있을경우 iterator 를 이용해서 삭제할수도 있다. 다음과 같이 2 개의 iterator 를 이용해서 특정범위의 요소를 모두 삭제할수도 있으며
void erase (iterator first, iterator last);
컨테이너에 포함된 모든 요소를 삭제할수도 있다.
void clear();
clear() 는 erase(begin(), end()) 로 바꾸어 쓸수도 있다.


2.1.3.3절. 원소의 크기 관련

현재 컨테이너에 포함된 원소의 크기를 알려주기 위해서 2개의 함수를 제공한다.

size_type size() const;
bool empty() const;
size() 는 현재 컨테이너에 포함된 원소의 갯수를 알려준다. empty()는 컨테이너에 원소가 있는지 없는지 확인한다. 원소가 없다면 1 그렇지 않을경우 0을 되돌려준다. empty() 조사는 size() == 0 을 통해서도 검사할수 있겠지만, 단지 원소의 유무를 조사하길 원한다면 empty() 를 사용하도록 하자.


2.1.3.4절. 기타 함수

swap()을 제공한다. 이것을 이용하면 요소를 덮어쓸수 있다.

void swap(map&)				
count()를 사용하면 해당 key 가 몇개의 값를 가지고 있는지 확인할수 있다. 그러나 map 에서는 하나의 key 에 하나의 값만을 가지게 되므로, 단지 key 의 존재유무를 파악하는정도로만 이용할수 있을것이다. 이 함수는 하나의 키가 여러개의 값을 가질수 있는 multimap 에 유용하게 사용할수 있을것이다.


2.2절. multimap

2.2.1절. 특징

key 를 중복해서 사용할수 있다는 점만 빼고는 map 과 동일한 특징을 가진다.


2.2.2절. 선언

multimap 는 다음과 같이 선언할수 있다.

표 2. 템플릿 Parameters

Parameter 설명 기본값
Key map 의 key 이다.  
Data Key 가 가르키는 데이타 이다.  
Compare Key 가 정렬되기 위해사용되는 크기 비교함수이다. 첫번째 아규먼트가 2번째 아규먼트보다 작으면 true 를 그렇지 않으면 false 를 리턴하도록 만든다. less<key>
Alloc multimap의 할당기 이다. map 은 이 할당기를 통해서 메모리 관리를 한다. alloc

2.2.3절. 자주사용하는 함수

기본적으로 map 과 동일한 함수를 사용한다. 다만 동일한 키를 가지게 됨으로 이를 지원하기 위한 추가적인 함수를 필요로 한다. 또한 map 와 같은 name[key] = value; 와 같은 삽입을 지원하지 않는다. 그러므로 insert 를 사용해야 한다.

#include <map>
struct ltstr
{
bool operator()(const char* s1, const char* s2) const
{
return strcmp(s1, s2) < 0;
}
};

int main()
{
multimap<const char*, int, ltstr> m;

m.insert(pair<const char* const, int>("a", 1));
m.insert(pair<const char* const, int>("a", 2));
m.insert(pair<const char* const, int>("b", 3));
m.insert(pair<const char* const, int>("c", 4));
m.insert(pair<const char* const, int>("a", 5));
}

하나의 key 가 여러개의 값을 가질수 있음으로, key 가 몇개의 값을 가지고 있는지 확인할수 있는 함수가 필요할 것이다. 그래서 count()라는 함수를 사용한다. 이함수도 map 에서 사용할수 있겠지만, map 에서의 용도라면 찾기원하는 key 가 있는지 없는지 정도가 될것이다.

size_type count(const key_type &k);
또한 키의 값을 가지고 오기 위해서 find()로는 부족할것이다. find 는 단지 처음에 일치된 key 의 iterator 값만을 돌려줄것이기 때문이다. 그래서 lower_bound()와 upper_bound 라는 함수가 중요하게 사용된다. 이들 두 함수는 map 에서도 사용할수 있지만 map 에서는 거의 사용할 일이 없다.
iterator lower_bound(const key_type& k);
const_iterator lower_bound(const key_type& k) const;
iterator upper_bound(const key_type& k);
const_iterator upper_bound(const key_type& k) const;
예를 들어 key "a" 가 가지는 모든 값을 가져오길 원한다면
multimap<const char*, int, ltstr>::iterator start;
multimap<const char*, int, ltstr>::iterator end;

start = m.lower_bound("a");
end = m.upper_bound("a");

while(start != end)
{
cout << start->first << ";" << start->second << endl;
*start++;
}
위의 코드는 count()와 find()를 이용한 코드로 대체가능할것이다.
multimap<const char*, int, ltstr>::iterator start;
start = m.find("a") ;
int count = m.count("a");
for (int i = 0; i < count; i++)
{
cout << start->first << ":"<< start->second << endl;
*start++;
}


2.3절. set

2.3.1절. 특징

map 과 달리 set 은 "키"가 곧 "값" 이된다. 이것은 어찌보면 list 와 매우 비슷하다. list 의 sort 버젼정도라고 해도 무방할듯하다.

set 은 키는 유일해야 한다. 만약 동일한 key 를 insert 하게 된다면, 덮어쓰게 된다.

set 은 특히 includes, set_union, set_intersection, set_difference 와 같은 집합관련 알고리즘과 자주 사용된다. 왜냐하면 list 와 달리 set 은 기본적으로 컨테이너의 요소들이 Sort 되어서 저장되는 Sorted Associative Containers 로써, 이들 알고리즘을 매우 효율적으로 적용시킬수 있기 때문이다(알고리즘에 대해서는 나중에 다루겠다.).

다음은 set_union 을 이용하여 두개의 set 컨테이너의 합집합을 구하는 예제이다.

#include <set>
#include <algorithm>
#include <vector>

struct ltstr
{
bool operator()(const char* s1, const char* s2) const
{
return strcmp(s1, s2) < 0;
}
};

int main()
{
set<char *, ltstr> mydata;
set<char *, ltstr> mydata2;
set<char *, ltstr> mydata3;

vector<char *> myunion;

mydata.insert("a");
mydata.insert("z");
mydata.insert("b");
mydata.insert("c");
mydata.insert("f");
mydata.insert("g");
mydata.insert("d");

mydata2.insert("k");
mydata2.insert("o");
mydata2.insert("e");

set_union(mydata.begin(), mydata.end(), mydata2.begin(), mydata2.end(),
back_inserter(myunion), ltstr());

for (int i = 0; i < myunion.size(); i++)
{
cout << myunion[i] << endl;
}
}


2.3.2절. 선언

Sequence 컨테이너의 선언에 단지 정렬을 위한, 함수만 하나 추가되었다고 보면 된다.

표 3. 템플릿 Parameters

Parameter 설명 기본값
Key set 의 key 이다. key 자체가 value 가된다.  
Compare set 요소가 정렬되기 위해사용되는 크기 비교함수이다. 첫번째 아규먼트가 2번째 아규먼트보다 작으면 true 를 그렇지 않으면 false 를 리턴하도록 만든다. less<key>
Alloc set의 할당기 이다. set 은 이 할당기를 통해서 메모리 관리를 한다. alloc

기본적인 사용방법은 위의 예제를 참고하면 될것이다.

set 도 정렬 연관 컨테이너 이므로 원소를 정렬할수 있도록 비교함수가 정의되어 있어야 한다. 물론 int, string 과 같은 타입의 원소를 컨테이너에 적재 할때는 굳이 비교함수를 정의할 필요가 없을것이다.


2.3.3절. 자주 사용되는 함수들

역시 다른 컨테이너들과 중복되는 비슷한 일을 하는 같은 이름의 함수들이 많으므로, 익히고 사용하는데 어려움이 없을것이다.


2.3.3.1절. 위치 접근 관련

map 와 마찬가지로 "첨자" 접근이 불가능하므로 iterator 를 통해서 접근해야 한다.

begin()과 end()를 사용해서 각각 컨테이너의 처음과 마지막의 위치를 되돌려줄수 있다.

iterator begin();
iterator end();
이들은 다음과 같이 사용가능할 것이다.
#include <set>
struct ltstr
{
bool operator()(const char *s1, const char *s2) const
{
return strcmp(s1, s2) < 0;
}
};

int main()
{
set<char *, ltstr> mydata;
mydata.insert("a");
mydata.insert("b");
mydata.insert("c");

set<char *, ltstr>::iterator mi = mydata.begin();

while(mi != mydata.end())
{
cout << *mi << endl;
*mi++;
}
}

특정키의 위치를 알고 싶을때는 find() 함수를 사용하면 된다. 이함수를 사용하면 해당키의 위치를 가르키는 iterator 를 리턴한다. 만약 찾는 값이 없다면 NULL 을 리턴한다.

iterator find(const key_type &k) const


2.3.3.2절. 삽입 / 삭제 관련

삽입을 위해서 insert()라는 함수를 사용한다. 만약에 동일한 키가 컨테이너 안에 있다면 겹치게 되고, 그렇지 않다면 insert 하게 된다. insert()할때 원소는 비교함수에 의해서 자동적으로 정렬이 된다.

pair<iterator, bool> insert(const value_type& x)
삭제를 위해서 erase() 함수를 사용한다.
	
void erase(iterator pos);
size_type erase(const key_type& k);
void erase(iterator first, iterator last);
key 값으로 지울수 있으며, iterator 로 특정한 위치를 지정하거나, 특정한 범위내(first 와 last)의 원소를 삭제할수 있다. 모든 원소를 삭제하기 원한다면 clear()를 사용한다.
void clear()


2.3.3.3절. 원소의 크기 관련

현재 컨테이너에 포함된 원소의 크기를 알기 위해서 사용한다.

size_type size() const;
bool empty() const;
원소가 있는지 없는지만 확인하기 원한다면 empty()를 사용하도록 하자. 이외에도 count()라는 함수가 있다. 이것은 동일한 key 값을 가지는 원소가 몇개있는지를 검사하고자 할때 사용하는데, set 은 한번에 하나의 key 만 가질수 있으므로 별로 사용할 필요가 없을것이다. 이것은 multiset 컨테이너에 유용하게 사용될수 있을거이다.


2.4절. multiset

2.4.1절. 특징

하나의 키 값만을 가질수 있는 set 에 비해서 동일한 여러개의 키값을 가질수 있다. 그 점을 제외하고는 set 과 동일하게 사용될수 있다.

set 과 마찬가지로 집합관련 알고리즘과 함깨 유용하게 사용될수 있다.

#include <algorithm>
#include <vector>
#include <set>
int main()
{
const int N = 10;
int a[N] = {4, 1, 1, 1, 1, 1, 0, 5, 1, 0};
int b[N] = {4, 4, 2, 4, 2, 4, 0, 1, 5, 5};

multiset<int> A(a, a+N);
multiset<int> B(b, b+N);
vector<int> C;

set_union(A.begin(), A.end(), B.begin(), B.end(), back_inserter(C));

for (int i = 0; i < C.size(); i++)
cout << C[i] << endl;

C.clear();
set_intersection(A.begin(), A.end(), B.begin(), B.end(), back_inserter(C));
cout << "==========" <<endl;
for (int i = 0; i < C.size(); i++)
cout << C[i] << endl;
}
무지무지 쉽게 2개의 컨테이너간의 합집합과, 교집합을 구한걸 볼수 있다.


2.4.2절. 자주 사용되는 함수

set 과 동일하다. 다만 count() 함수와 lower_bound()와 upper_bound()등의 함수등을 좀더 현실적으로 사용할수 있다.

size_type count(const key_type& k) const
iterator lower_bound(const key_type& k) const
iterator upper_bound(const key_type& k) const
사용방법은 map 에서 본것과 같으니 자세한 설명은 생략하도록 하겠다.


3절. 결론

이상 Association Container 에 대한 간략한내용에 대해서 알아보았다. 사실은 그럴듯한 예제를 하나 만들어서 설명하고 싶었지만 컨테이너에 대한 내용자체가 방대한 관계로 사용방법을 익히는데 무리 없을 정도의 코드로 설명을 대신하였다.

이 다음장에서는 지금까지 STL 을 다루면서 자주 보아왔던 iterator 와 알고리즘에 대해서 알아보도록 하겠다(우리는 이미 알고리즘을 이번장에서 사용했다. set_union 과 같은 것들이 그것이다).

:::
2007/05/22 22:27

STL - Sequences 컨테이너

출처 : Joinc Wiki

STL(2) - Seuences 컨테이너

윤 상배

dreamyun@yahoo.co.kr


차례
1절. Container 에 대해서
2절. Sequence 컨테이너
2.1절. Sequence 컨테이너란
2.1.1절. vector
2.1.1.1절. 특징
2.1.1.2절. 선언
2.1.1.3절. 자주 사용되는 멤버 함수들
2.1.2절. deque
2.1.2.1절. 특징
2.1.2.2절. 선언
2.1.2.3절. 자주 사용되는 멤버 함수들
2.1.3절. list
2.1.3.1절. 특징
2.1.3.2절. 선언
2.1.3.3절. 자주 사용되는 멤버 함수들
3절. 결론

1절. Container 에 대해서

STL 을 설명하면서 등장하는 컨테이너(저장소)란 객체를 원소로 저장할수 있는 영역을 말한다. 이는 특정 데이타 타입만을 저장할수 있는 배열과는 상당히 틀린개념이다. C++(혹은 객체지향플밍) 을 해봤다면 알겠지만 단순히 말해서 객체란 묘사될수 있는 모든 사물을 말한다. 이러한 객체를 원소로 저장할수 있다는 특성때문에 컨테이너는 모든 종류의 데이타저장이 가능하도록 되어있다. 이러한 특성은 앞의 STL(1)에서 언급했듯이 C++ Template 을 이용해서 구현된다(더 자세히 말하자면 Class Template).

STL 은 다양한 자료구조로 사용될수 있는 약 10가지 정도의 컨테이너들을 제공한다. 여기에는 배열을 일반화 시킨 vector, queue 로 사용가능한 deque, 요소를 가르키는 첨자로 int 형대신에 다른 형(char * 이나 string)을 사용할수 있는 map 등.. 이있으며, 이러한 다양한 컨테이너들을 사용하여서 쉽게 확장 가능한 자료구조를 단 몇줄의 코딩으로 쉽게 구현할수 있다.

이러한 10가지 정도 되는 컨테이너들은 다시 Sequence 컨테이너와, Association 컨테이너로 크게 나눌수 있다. 앞으로 이 두가지 종류의 컨테이너를 기준으로 해서 다양한 컨테이너들의 사용방법에 대해서 알아보도록 할것이다. 그러나 이번문서에서는 Sequence 컨테이너만을 다룰 것이며 Association 컨테이너에 대한 내용은 다음 기사에서 다루게 될것이다.


2절. Sequence 컨테이너

2.1절. Sequence 컨테이너란

Sequence 컨터이너에는 vector, deque, list, stack 등이 포함되어 있다. 이들을 Sequence 라고 부르는 이유는 배열과 같이 선형적으로 데이타가 나열되기 때문이다. 각 컨테이너의 이름에서 눈치를 챘겠지만 deque 는 queue 자료구조를 위해서, list 는 linked list, stack 는 stack 자료구조를 지원할수 있는 컨테이너이다. 이들 Sequence 컨테이너들은 배열과 매우 비슷한 성질을 지니고 있으며, 배열처럼 첨자로 접근이 가능하다.

컨테이너들은 대부분 공통의 인터페이스(함수)를 제공한다. 함수의 이름도 똑같고, 하는일도 같은경우가 상당히 많다. 그러므로 하나의 컨테이너를 사용하는 법만 확실하게 익혀도, 나머지 컨테이너들은 별로 어렵지 않게 익힐수 있을것이다. (예를들어 vector 에서 마지막에 원소 입력을 위해서 쓰이는 push_back() 은 list, deque, stack 등에도 존재하며, 동일한 일을 한다.


2.1.1절. vector

2.1.1.1절. 특징

vector 은 map 과 함께 가장 자주 사용되는 컨테이너이다. 단순히 배열을 모든 자료형을 원소로 가질수 있도록 일반화 시킨것이라고 보면된다. 배열과 같이 첨자연산이 가능하다. 각각의 요소에 랜덤하게 접근이 가능하며, vector 의 마지막 요소를 삭제하는데는 상수시간이 소요된다. 반면 처음과 중간에 있는 요소를 삭제하는데에는 linear time(선형시간) 이 소요된다.

처음과 중간에 있는 요소를 삭제하는데에 linear time 이 걸리는 이유는 배열과 매우 비슷하게 만약 중간에 있는 요소를 삭제 했을경우 그 뒤에 있는 요소들을 모두 한칸씩 앞으로 밀어넣어야 하는 연산을 필요로 하기 때문이다(즉 마지막에 요소를 추가하거나 삭제할때 효율적임).

그러므로 만약 중간에 있는 요소를 자주 삭제하는 연산이 필요할경우에 vector 를 사용하는건 매우 비효율적이 될수 있다. 이럴경우에는 list 같은 다른 컨테이너를 사용해야 할것이다.

STL에서 제공하는 다른 컨테이너들과 마찬가지로 vector 도 자신이 알아서 메모리 관리를 한다.


2.1.1.2절. 선언

vector 컨테이너는 다음과 같이 선언되어 있다.

표 1. 템플릿 Parameters

Parameter설명기본값
T백터의 원소로 들어가게될 값의 (type)이다. 이 값은 객체가 될수도 있다. 
Alloc벡터의 할당기(allocator)이다. 이 할당기를 통해서 자동으로 메모리 관리를 한다.alloc
보통은 기본 할당기를 사용하게 됨으로 다음과 같이 벡터를 만들어서 사용할수 있다.
vector<int> myint;
vector<class myobj> myobj;
vector<vector <myint> > v_myint;
위의 선언을 보면 알겠지만 int 같은 자료형은 물론이고 class, 또다른 컨테이너까지도 멤버 요소로 가질수 있다. vector의 vector 을 사용하면 마치 2차원 배열처럼 사용할수 있을것이다.

또한 vector 은 최초에 생성될때, 기본적인 크기와 채워질 값을 지정할수 있다.

vector<string> mydata(20, "hello");
위의 경우 mydata 의 크기는 20만큼 크기가 미리 할당되고, "hello"로 모두 초기화 될것이다.


2.1.1.3절. 자주 사용되는 멤버 함수들

위에서 보면 vector 를 단지 특정 (type)에 관계없이 멤버 요소를 가질수 있는 배열 정도로만 치부할수도 있을것이다. 그러나 vector 은 클래스 템플릿의 특징을 살려서 매우 다양한 함수들을 제공하며, 우리는 이러한 함수들을 통해서 별도로 코딩할필요 없이 원하는 작업을 할수 있게 된다.

이러한 함수들은 굉장히 많기 때문에 비교적 사용빈도가 높은 것들만을 설명하도록 하겠다.


2.1.1.3.1절. 위치 관련

vector 에서 원소에 접근하기 위한 가장 간단한 방법은 배열과 같이 배열첨자를 이용하는 방법이다. 그러나 이와는 달리 포인터와 같이 접근이 가능하다. 이러한 접근을 위해서 iterator 라는 것을 제공한다.

iterator 는 포인터와 매우 비슷하게 원소의 위치를 가르키는 역활을 한다. iterator 는 다음과 같이 선언되어서 사용할수 있다.

#include <vector>
#include <string>

int main()
{
vector<string> mydata;

// iterator 을 만든다.
// iterator 로 순환(참조)할 컨테이너의 타입에 맞게 만들어준다.
vector<string>::iterator mi;

mydata.push_back("hello");
mydata.push_back("world");
mydata.push_back("ok");

mi = mydata.begin(); // mydata 의 첫번째 원소의 위치를 가르킨다.
while ( mi != mydata.end())
{
cout << *mi <<endl;
mi++;
}
cout << mydata[0] << endl; // 배열과 같이 첨자로 접근 가능하다.
}
begin()은 vector 컨테이너의 처음위치를 가르키는 iterator 를 넘겨주며 end()를 통해서 마지막위치를 가르키는 iterator 를 가져올수 있다. 위의 예를 보면 iterator 가 pointer 와 아주 유사함을 알수 있다. 포인터 처럼 다음 위치를 가르킬수도 있으며 증가연산도 사용할수 있다.
iterator begin();
iterator end();
이들 begin() 과 end()는 다른 모든 컨테이너에도 동일하게 적용된다.


2.1.1.3.2절. 삽입/삭제 관련

vector 은 삽입과 관련해서 2개의 함수를 제공한다. insert 와 push_back 인데, 이들함수는 다른 sequence 컨테이너에 같은이름으로 제공되고 있으며, 자주 사용되는 함수들임으로 익혀두도록 하자. push_back 는 컨테이너의 마지막에 원소를 삽입할때 사용하며, insert 는 임의의 지역에 원소를 삽입하고자 할때 사용한다.

앞에서 말했다 시피, vector 의 경우 중간에 원소를 삽이시키는건 상당히 비효율적이다. 삭제할때와 마찬가지로 중간에 insert 시킬경우 insert 위치의 뒤에 있는 모든 원소의 위치가 재 조정되기 때문이다. 즉 insert 위치의 뒤에 100개의 원소가 더 있다면, 100번의 복사가 더 일어나게 된다.

void push_back(const T&);
iterator insert(iterator pos, const T& x);
push_back() 는 그냥 T 타입의 데이타를 밀어 넣기만 하면 그걸로 끝이다. insert 는 insert 할 위치를 iterator 를 통해서 지정해 주어야 한다.
vector<string> mydata;

mydata.push_back("hello");
mydata.push_back("world");
mydata.push_back("ok");

// hello 다음에 "yundream" 을 삽입한다.
mydata.insert(mydata.begin()+1, "yundream");

// 혹은 iterator 을 선언해서 삽입가능하다.
vector<string>::iterator mi;
mi = mydata.begin();
mydata.insert(mi + 2, "hahaha");
삭제관련 함수로는 pop_back() 과 erase(), clear()가 있다. pop_back()는 마지막 원소를 삭제할때 사용하며, erase()는 임의의 위치에 있는 원소를 삭제할때 사용된다. erase() 는 또한 지정된 범위에 있는 원소들을 지울수도 있다. clear()는 모든 원소를 지울때 사용된다.
void pop_back();
iterator erase(iterator pos);
iterator erase(iterator first, iterator last);
clear()는 erase(begin(), end()) 와 동일하게 쓰일수 있을것이다.

swap()을 이용하면 현재 벡터의 내용을 다른 벡터로 대신 채울수 있다.

void swap(vector&); 
vector<string> ok1, ok2;
...
...
ok1.swap(ok2); // ok1 의 내용을 ok2로 덮어쓴다.


2.1.1.3.3절. 원소의 크기관련

size()를 통해서 현재 vector 컨테이너에 몇개의 원소가 들어있는지 계산해 낼수 있다. empty()를 이용하면 현재 원소가 있는지 없는지를 알수 있다. 실제로 size() 를 통해서 원소가 비어있는지 알수도 있지만, empty()가 더 효율적이므로 단지 컨테이너가 비어있는지만 확인하고자 한다면 empty() 를 사용하도록 한다.

size_type size();
bool empty();
iterator 대신 size()를 이용해서 vector 컨테이너의 모든 원소를 가져올수도 있다.
vecotr<string> mydata;
mydata.push_back("1");
mydata.push_back("2");
mydata.push_back("3");
mydata.push_back("4");
for(int i = 0; i < mydata.size(); i++)
{
cout << mydata[i] << endl;
}

vector 의 경우 원소의 갯수가 늘어나면 자동적으로 컨테이너의 크기가 조절된다고 했는데, 하나의 원소가 들어올때 마다 컨테이너의 크기를 조절되는 것보다는, 어느정도 크기를 미리할당 해놓고 원소를 받아들이는게 훨씬더 효율적일 것이다.

이처럼 초기에 vector 의 크기를 어느정도 지정해주어서 크기조절(메모리할당)을 최소한도로 일어도록 조절할수 있다. reserve() 함수를 이용해서 이러한 일들을 할수 있다. 한마디로 말해서 사용자가 사용될 메모리의 양을 미리 계산해서 메모리의 크기를 재할당(reallocation)하는 거라고 보면 된다. 현재 할당된 메모리의 양(받아들일수 있는 원소의 크기)은 capacity() 함수를 이용해서 알아낼수 있다. reserve()에 의한 메모리 할당은 성능에 중요한 영향을 미치므로 vector 뿐만 아니라 다른 컨테이너를 쓸때는 어느정도의 원소를 이용할것인지를 예측해서 적당한 크기를 할당하도록 하자.

void reserve(size_type n);
size_type capacity() const;
만약 reserve() 로 크기를 충분히 잡지 않아서 원소의 갯수가 초과했을경우에는 현재 capacity()*2 만큼의 메모리가 한꺼번에 할당된다. 처음에 reserve(20) 을 잡았다면 원소가 20을 초과했을경우에는 20*2 만큼이 할당될것이다. 이것이 초과된다면 다음번에는 40*2 만큼이 할당된다.


2.1.2절. deque

2.1.2.1절. 특징

이름에서도 알수 있겠지만 queue 자료구조를 위해 최적화 되었다. 기본적인 사용방법은 vector 와 매우 같으며, vector 와는 달리 컨테이너의 맨 앞에서 행해지는 삽입/삭제에 대해서 매우 빠른 연산(상수시간)이 가능하다. 보통의 queue 자료구조는 맨앞에 원소의 삽입을 하지 않는게 원칙이지만 deque 는 맨앞에 원소의 삽입도 지원한다.

이는 자료구조의 맨의 요소를 빈번하게 읽고/삭제 해야하는 queue 의 특성을 효율적으로 지원할수 있어야 하기 때문이다.

이외에는 vector 와 완전히 사용방법이 똑같다고 보면 된다. vector 과 마찬가지로 각 요소에 랜덤하게 접근가능하다. vector 로 만들어진 코드를 dequeu 로 변경하고 싶으면 선언에서 vector 만 deque 로 변경시켜 주면 된다.

하지만 중간에 요소를 삽입/삭제하는데에는 vector 와 같이 선형시간이 소비된다. 중간에 요소 삽입 ,삭제 작업이 많이 요구된다면 vector 이나 deque 대신 list 를 쓰는게 더욱 효율적이다. vector 과 마찬가지로 생성되면서 할당기가 만들어져서 자동적으로 메모리를 관리한다.


2.1.2.2절. 선언

vector와 같다.

표 2. 템플릿 Parameters

Parameter설명기본값
Tdeque의 원소로 들어가게될 값의 (type)이다. 이 값은 객체가 될수도 있다. 
Allocdeque의 할당기(allocator)이다. 이 할당기를 통해서 자동으로 메모리 관리를 한다.alloc
기본할당기를 사용함으로 다음과 같이 deque 를 만들어서 사용할수 있다.
deque<int> myint;
deque<class myobj> myobj;
deque<vector <myint> > v_myint;

2.1.2.3절. 자주 사용되는 멤버 함수들

모든 사용방법 심지어는 함수 이름들까지도 vector와 똑같다. 위의 vector 를 설명한곳에서 vector 을 deque로만 변경시키면 된다.

vector 에서는 제공되지 않는 함수로 pop_front()와, push_front()가 있다. pop_front()는 맨앞의 원소를 삭제할때, push_front() 는 맨앞에 원소를 삽입할때 사용한다.

void push_front(const T&);
void pop_front();
궁금한게 하나 있다. 왜 vector 에서는 push_front 나 pop_front 를 지원하지 않는것일까. 지원하려고 맘만 먹으면 insert()나 erase() 등을 이용해서 충분히 구현할수 있을건데.. 그 이유는 vector에서 이러한 연산은 대단히 비효율적이기 때문이다. 그래서 처음부터 프로그래머 이러한 비효율적인 접근을 차단하기 위한 목적으로 이들 함수를 만들어 놓지 않은 것이다.
deque<int> myque;

myque.push_back(1);
myque.push_back(2);
myque.push_back(3);
myque.push_back(4);
myque.push_back(5);

cout << myque.size() << ":" << myque[0] << endl;
myque.push_front(0);
cout << myque.size() << ":" << myque[0] << endl;

myque.pop_front();
cout << myque.size() << ":" << myque[0] << endl;
myque.pop_front();
cout << myque.size() << ":" << myque[0] << endl;

deque<int>::iterator mi;
mi = myque.begin();

while( mi != myque.end())
{
cout << *mi << endl;
*mi++;
}


2.1.3절. list

2.1.3.1절. 특징

STL 에서 제공하는 list 는 double linked list 를 일반화 시킨 템플릿 버젼이라고 생각하면 된다. double linked list 의 특징을 또한 그대로 가지고 있다. vector 와 deque 는 임의 접근이 가능하다고 했다. 이는 vector, deque 가 배열의 특징을 가지기 때문으로 첨자를 이용한 임의 접근이 가능하다. 그러나 list 는 이러한 임의 접근을 할수가 없다(doublke linked list 도 마찬가지). 대신에 중간에서의 요소 삽입/삭제를 매우 효율적으로 처리할수 있다. vector, deque 와 같은 메모리 연산이 일어나지 않기 때문이다. 단지 다음요소를 가르킬수 있도록 포인터만 재조정해 주면 되기 때문이다.

임의 접근이 안되므로 당연히 첨자를 이용해서는 요소에 접근할수 없다. 단지 이터레이터를 이용해서 접근이 가능하다. double linked list 의 요소에 접근하기 위해서는 포인터를 이용해야 하는것과 마찬가지이다.

임의 접근이 포기되는대신에 linked list 의 특징을 이용할수 있는 다양한 다른 기능들이 함수로 제공되어 진다.

그러나 list 도 단점이 있는데, 특 특정 원소에 접근하기 위해서는 처음부터 원소를 쭉 찾아서 접근해야 한다는 점이다. 임의 접근이 안된다. 그러므로 원소들이 서로 멀리 떨어져 있는 상황에서 연산을 하게 되면 비효율적이 될수가 있다.


2.1.3.2절. 선언

다른 vector 와 deque 와 동일하다.

표 3. 템플릿 Parameters

Parameter설명기본값
Tdeque의 원소로 들어가게될 값의 (type)이다. 이 값은 객체가 될수도 있다. 
Allocdeque의 할당기(allocator)이다. 이 할당기를 통해서 자동으로 메모리 관리를 한다.alloc

2.1.3.3절. 자주 사용되는 멤버 함수들

list 의 linked list 로써의 특징때문에 원소의 삽입/삭제에 대해서 가장 폭넓은 지원을 해준다. push_back(), push_pop(), pop_front(), pop_back() 등의 모든 함수들을 지원하며, 중간에 요소를 입력하기 위한 erase(), insert() 도 지원한다. 물론 erase 와 insert 는 vector 과 deque 에서도 지원하지만 효율에서 많은 차이가 난다. list 는 이러한 모든 연산을 상수시간에 끝낼수 있다. 링크만 다시 만들면 되기 때문이다.

이렇게 메모리 재배치를 통하지 않고 링크 재배치만을 통해서 요소의 삽입/삭제가 가능하다는 장점때문에 다른 컨테이너에서는 제공하지 않는 몇가지 특별한 함수들을 제공한다.

void reverse();
void splice(iterator position, list<T, Alloc>& x);
reverse() 는 원소를 역으로 재배치하고, splice() 는 iterator 로 주어진 포지션부터 x list 를 결합시킨다. 이러한 모든 재배치 과정은 단지 링크만 재조합하는 것으로 상수시간에 이루어진다.
#include <list>

int main()
{
list<int> mylist;
list<int> mylist2;

mylist.push_back(0);
mylist.push_back(1);

mylist2.push_back(2);
mylist2.push_back(3);

// mylist 마지막에 mylist2 를 붙인다.
mylist.splice(mylist.end(), mylist2);

list<int>::iterator mi;
mi = mylist.begin();

while( mi != mylist.end())
{
cout << *mi << endl;
*mi++;
}

// mylist 를 역으로 재배치 한다.
mylist.reverse();
mi = mylist.begin();

while( mi != mylist.end())
{
cout << *mi << endl;
*mi++;
}
}


3절. 결론

이상 sequence 컨테이너에 대해서 알아보았다. 보면 알겠지만, 얼마나 유연하게 자료구조를 구현할수 있는가를 알수 있을것이다. 링크드 리스트이건 큐이건 스택이건 간에 고민없이 구현가능하다.

이와 함께 나중에 사용될 저너릭 알고리즘을 사용하면 자료구조에 각종 알고리즘까지 적용시켜서 사용할수 있다.

컨테이너를 사용할때 가장중요한것은 작업에 가장 효율적인 컨테이너를 사용해야 한다는 것이다. 사실 vector 만으로도 list, deque 를 구현가능하다. 그럼에도 list 와 deque 가 존재하는 것은 각각의 자료구조에 대한 요소의 연산에 적당한 컨테이너가 따로 존재하기 때문이다.

배열을 일반화 시켜서 간단하게 사용하고 싶다면 vector 을 queue 와 같이 처음 과 마지막에 요소를 추가/삭제 시켜야 한다면 deque 를 linked list 와 같은 복잡한 요소의 연산을 필요로 한다면 list 를 사용하면 된다.

여기에서 다룬 내용들은 Sequence 컨테이너의 기능중 일부분이다. 자세한 내용은 STL 홈페이지 를 참고하도록 하자.

:::
2007/05/14 03:44

STL 소개

오늘부터 STL 스터디 들어갑니다. 많은 관심 부탁드려요. 원문 : Joinc Wiki

STL(1) - 개요

윤 상배

dreamyun@yahoo.co.kr



1절. 소개

그동안 이사이트의 몇개 문서에서 STL 에 대한 언급이 있었지만, 매번 그냥 이런것이 있다.. 라는 정도로 하고 넘어갔었다. 이번에는 STL에 대한 직접적인 내용을 다루려고 한다.

STL 에 대해서 제대로 다루기 위해서는 C++ 에 대한 기본적인 이해와 템플릿 에 대한 이해를 필요로 한다. (템플릿, 클래스, 오버로딩, 함수포인터 등에 대한 이해를 필요로 한다.) 그리고 STL 전체를 제대로 다루려면 상당히 많은 시간을 투자해야 한다. 보통 STL 주제만으로도 책 한두권 정도는 간단하게 나올것이다(실제로 STL을 주제로한 몇권의 책이 시중에 나와있다).

그러므로 STL 에 대한 강좌는 몇번의 기사에 나누어서 이루어지게 될것이다. 이번 기사는 STL 에 대한 소개와 템플릿 프로그래밍에 대한 내용들을 다루게 될것이고, 다음번의 기사는 "sequences 컨테이너", "associative 컨테이너", "Iterator", "알고리즘", "그밖의 STL 에 대한 기술적인 다른 내용들" 이 될것이다. 전체적으로 5-6회에 걸쳐서 연제에 들어가게 될것이다.


2절. STL 소개

2.1절. STL 이란 무엇인가

STL 은 Standard Template Library 라고 불리우며, C++ 에서 사용할수 있는 컨테이너(container) class 와, 알고리즘 일반화 시켜서 사용할수 있는 자료구조 등을 포함하는 라이브러리의 모음이다. STL 은 흔히 generic library(일반화된 라이브러리) 라고 불리우며, 이러한 일반화를 구현하기 위해서 C++ 에서 제공하는 Template(템플릿) 을 사용하고 있다.


2.1.1절. 왜 STL 을 사용하는가

전용의 함수 혹은 전용의 자료구조가 아닌 일반적으로 사용 가능한 함수 혹은 자료구조를 만들기 위해서이며, 이러한 일반적인 자료구조에 일반적인 알고리즘을 적용시키기 위해서 이다.

말이 복잡한데 간단히 말해서, 프로그래밍 시간을 극적으로 단축시키고 코드수를 줄이고 유지보수 쉽고 확장 쉬운 코드를 만들기 위해서이다. 한마디로 인생을 편히 즐기기 위해서 STL 을 사용한다. 예를들어 여러분이 queue 자료구조를 구현한다고 해보자. 머 전산수업시간에 충실히 했다면, 그리고 시중에 나와있는 몇가지 책들을 열심히 섭렵했다면 만들수는 있겠지만, 개인적인 생각인데, 최소한 수백라인 이상의 코드를 그려내어야 할것이다. 코드수가 늘어났으니 그만큼 버그일어날 확률도 많고, 이것저것 잡다하게 신경써야할게 한두개가 아닐것이다. 게다가 다른 자료구조를 지원하도록 해야 한다면, 그때 마다 각각의 자료구조를 위한 새로운 전용의 함수들을 생성해 내야만 할것이다. 매크로를 이용해서 혹은 클래스를 이용해서 어느정도 흉내내기는 가능하겠지만, 이건 어디까지나 흉내일 뿐이다.

이걸 STL 에서 제공하는 deque 를 이용하면 10줄 내외로 작성 가능하다. 크기가 얼마나 되어야 할지 걱정할 필요도 없고, 메모리 뻑나지 않을지, queue 를 어떻게 구성해야 할지에 대해서 고민할 필요 없이 그냥 선언해서 사용하면 된다. 게다가 표준이다. Linux 에서 작성한거 그대로 솔라리스에서 컴파일만 하면 제대로 작동한다. Windows 에서도 마찬가지이다(윈도우의 경우 STL 라이브러리를 별도로 설치해야 하는걸로 알고 있다).

예제 : queue.cc

#include <deque>

int main()
{
deque<int> myque;

myque.push_back(100);
myque.push_back(200);
cout << myque[0] << endl;
myque.pop_front();
myque.push_back(300);
myque.push_back(400);
cout << myque[0] << endl;
myque.pop_front();
cout << myque[0] << endl;

cout << "Size " << myque.size() << endl;
}
위의 코드를 보시라 실질적으로 보면 queue 자료구조를 위해서 단지 한줄만을 사용하고 있음을 알수 있다. 단지 한번의 선언으로 queue 자료구조를 만들었다. 만약 큐에 저장할 자료가 문자열이여야 한다면 ? deque<char *> mychar; 이 한줄로 끝이다. 구조체, 클래스 는 물론이고 다른 STL 컨테이너 까지도 멤버 자료로 가질수 있다.

위의 queue 같은 경우는 STL 을 사용하지 않더라도 그마나 구현이 용이하다. 그렇지만 map(인덱스가 int 형이 아닌 스트링같은) 같은걸구현하려고 한다면 이거 보통일이 아닐것이다. 이것역시 STL 을 이용하면 선언으로 끝이다.

결론적으로 말해서 STL을 사용하면 그렇지 않을때 보다 50% 이상의 시간절약이 가능하다. 게다가 아드레날린의 생성도 획기적으로 줄여준다. 특히 데이타 저장과 자료구조를 위한 container 의 제작과 사용에 있어서 획기적인 방법을 제공해 준다.


2.1.2절. STL 은 어떻게 구현되었는가

STL 은 클래스를 통해서 이루어진게 아니다 (STL 템플릿 들도 클래스로 만들어 졌다. 여기에서 말하는 것은 모든 기능의 구현을 위해서 클래스만을 전적으로 사용한게 아니란 뜻이다. STL에서 클래스는 거의 메서드와 자료형을 함께 두기 위한 구조체 수준에서 사용된다.). C++ 에서 제공하는 Template 를 이용해서 이루어졌다. 이 template 에 대해서는 따로 장을 할당해서 설명할것이다.


2.1.3절. STL의 장단점

모든게 그렇지만 STL 역시 장점과 더불어 몇가지의 단점을 가지고 있다.


2.1.3.1절. 장점

장점으로는 우슨 소스크기의 축소를 들수 있다. 이미 STL에는 자주 사용되는 50여개 정도의 알고리즘과 다양한 데이타 구조들을 가지고 있다. 우리는 이러한 STL에서 사용하는 자료구조와 알고리즘을 이용해서 쏘스 코드의 크기를 획기적으로 줄일수 있다.

STL 알고리즘과 컨테이너들은 기존의 C/C++ 의 포인터와 배열에도 사용할수 있으며, 변환 가능하므로 유연하게 사용가능하다. 그리고 컨테이너와 알고리즘이 서로 분리되어 있음으로(많은경우), 자신이 알고리즘을 만들어서 컨테이너에 적용시키는 작업이 가능하다.

대부분의 알고리즘과 자료구조들은 충분히 테스트되고 최적화 되어 있다. 또한 효율적인 프로그래밍을 위한 다양한 접근 방법을 제공하므로, C++ 을 사용한 STL 이 느릴것이라는 예상을 깨고 약간만 신경을 쓴다면 매우 효율적인 코드를 생성할수 있다.


2.1.3.2절. 단점

우선 디버깅이 어렵다. STL을 잘못사용할경우 발생되는 컴파일시의 에러코드는 템플릿 깊숙이에 있는 내부함수들의 에러들을 출력시킨다. 이러한 에러가 발생하면 일단 에러메시지의 양들도 많을 뿐더러, 이해자체가 어려운 메시지들이다. 이걸 해결하는 방법은 하나뿐이다. STL 에 익숙해지기..

STL이 유연성을 구현하기 위한 템플릿의 경우 프로그램의 크기를 매우 크게 만들수 있다. 아직은 컴파일러가 템플릿을 그리 효율적으로 처리하지 못하기 때문이다. 역시 비용을 잘 고려하여서 효율적으로 STL의 알고리즘과 컨테이너들을 사용해야 한다.

반복자(나중에 다룰것임, 일종의 포인터)와 컨테이너가 분리되어서 존재한다. 이는 쓰레드 프로그래밍시 문제를 발생할 소지를 가지고있다.


3절. Template

사실 템플릿도 그리 만만한 내용이 아니다. 좀두꺼운 C++ 책같은 경우 왠만한 얇은 책 한권 분량 정도를 할애해서 템플릿을 설명한다. 이 문서는 템플릿 자체를 설명하는 문서는 아니므로 그렇게 자세히 설명하지는 않을 것이다. 다만 STL을 좀더 쉽게 이해하도록 돕는 수준에서만 설명을 할것이다. 템플릿에 대한 자세한 내용은 다른 문서나, 책을 이용하도록 하자. (비록 C 와 C++ 을 다른 언어라고 말을 하긴 하지만) C 에 비해서 C++ 에서 크게 추가된 것이 있다면 바로 "클래스" 와 "템플릿" 정도가 될것이다.


3.1절. Template 에 대해서

만약에 우리가 int 형의 숫자를 받아들이는 A 라는 함수를 만들었다고 가정을 하자. 이 A 라는 함수는 int 아규먼트를 받아들일 경우는 문제가 없지만, 이 A 라는 함수는 단지 int 형만을 처리할수 있다는 문제점을 가지게 된다. float, double 등이 인자로 들어오게 되면 제대로 처리를 할수 없을것이다. 이럴경우 어쩔수 없이 전용의 새로운 함수를 만들어야만 할것이다. 물론 C++ 의 특성중 하나인 멤버함수의 오버로딩을 이용하면 어느정도의 이러한 문제를 해결할수 있을것이다. 그러나 만약 struct 나, string, class 등을 처리하는 함수를 만들어야 한다면? 이쯤 되면 함수 오버로딩 정도로 그리 간단하게 해결될 문제가 아니다(프로그래밍에서 대부분의 것들이 그렇듯이 물론 불가능한건 아니다. 하지만 너무 많은 비용이든다. 너무 많은 시간, 너무 많은 노력을 필요로 한다)

어쨋든 Template 이 아규먼트로 주어지는 형에 관계없이 받아들일수 있다는 것 때문에, 특히 데이타 저장소(container)로 유용하게 쓰인다. STL 역시 Template 의 이런 특성을 이용해서 범용적으로 사용가능한 저장소를 제공한다(vector, map, list, queue 등등..)

STL 에서는 보통 이러한 저장소를 만들기 위해서 class template 를 이용하며 알고리즘을 위해서 함수 template 를 이용한다. 저장소를 위해서 class template 를 이용하는 이유는 class 가 데이타부분과 메써드 부분을 분리할수 있음으로 아무래도 데이타를 저장하기가 방법상 더 간단하기 때문이다. 알고리즘을 이용해서 함수 template 를 이용하는 이유는 template 의 특성을 이용해서 인자에 관계없이 범용적으로 사용가능한 알고리즘을 만들수 있기 때문이다.


3.2절. 템플릿 작성

3.2.1절. 함수 템플릿

작성방법은 간단하다. C++ 에서는 템플릿의 제작을 위해서 "template" 라는 키워드를 제공해준다.

예제 : myadd_template.cc

#include <string>

template <typename T>
T myadd(T x, T y)
{
return x + y;
}


int main()
{
cout << myadd(4, 6) << endl;
cout << myadd(1.6, 1.8) << endl;
}
위의 예제를 테스트 해보면 데이타 형에 관계없이, '+' 연산이 가능함을 알수 있다. 즉 myadd(2, 3), myadd(3.5, 5.98) 등 int, float 등의 데이타 형에 신경쓸필요가 없이 연산이 가능하다. 여기에는 '+' 연산자를 사용할수 있는 모든 자료형이 사용가능하다. 예를 들어 STL 에서 제공하는 string 의 경우 문자열간 의 '+' 연산이 가능하다. 그러므로 다음과 같이 문자열을 더하기 위해서도 사용할수 있을것이다.
  
string b2="hello";
string b3=" world";
cout << myadd(b2, b3) << endl;
그러나 char * 형같은경우는 '+' 연산을 취할수 없기 때문에 char *을 인자로 취할경우에는 연산자 오류를 발생시키며 컴파일 실패할것이다.

그리고 위의 예제는 또다른 문제를 가지고 있다 typename T 가 서로 같은 종류의 형을 가져야 된다는 것이다. (int, int), (float, float), (string, string) 등을 사용하는데 있어서는 문제가 없지만 (int, float) 와 같이 서로 다른 형을 사용할때는 문제가 발생할것이다. 외냐하면 typename T 에 대해서 이러한 아규먼트는 지원이 되지 않기 때문에, 위의 아규먼트를 처리할수 있는 어떠한 함수원형도 발견할수 없다는 오류 메시지를 출력하며 컴파일 에러가 발생할것이다.

이럴경우에는 연산자 오버로딩을 통하여 해결 가능하다. 다음과 같은 연산자 '+' 에 대해서 정의를 해두면 된다.

bool operator+(myadd<double, long> &x)
{
return x.first + x.second;
}
물론 이경우에는 함수 템플릿으로는 안되고, 클래스 템플릿을 작성해야 할것이다.

3.2.2절. 클래스 템플릿

클래스 템플릿의 사용역시 매우 간단하다. 아래는 바로 윗 절에서 사용되었던 예제를 연산자 오버로딩 까지 포함시켜서, double, long 연산까지 가능하도록 약간 수정한 것이다.

예제 : class_tem.cc

#include <string>

template <typename T1, typename T2>
class cal
{
public:
bool operator+(cal<double, long> &x)
{
return x.first + x.second;
}

void sum(T1 x, T2 y)
{
cout << x + y << endl;
}

void dif(T1 x, T2 y)
{
cout << x - y << endl;
}

};

int main()
{
cal<int, int> data;
data.sum(1, 2);
data.sum('a', 'b');

cal<double, long> data2;
data2.sum(1.2, 2);

cal<double, double> data3;
data3.dif(1.2, 1.5);
}

하지만 클래스 템플릿의 진정한 사용용도는 데이타 저장소(container)를 만들기 위한 용도이다. 만약 queue 형식을 지원하는 데이타 저장소를 만들고 지원해야할 데이타 형의 종류가 여러가지라면, 우리는 데이타 형의 종류의 수만큼의 함수혹은 클래스를 만들어야 할것이다. 하지만 클래스 템플릿을 사용하면 이러한 수고를 덜수 있다. 템플릿을 이용한 container 의 제작을 위해서 다양한 데이터형을 저장할수 있는 arrary 데이타 구조를 가지는 어떤 저장소를 만들어 보도록 하겠다.

우리는 클래스 템플릿 를 이용해서 circular buffer 구조를 가지는 queue 자료구조를 구현할것이다. circular buffer 는 배열을 이용한 buffer 로 다음과 같은 구조를 가진다.

    +- data1    +- data3                      +- data8  
| | |
+-----+-----+-----+-----+-----+-----+-----+-----+
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
+-----+-----+-----+-----+-----+-----+-----+-----+
| |
| +- data2
+- data9
이것은 고리처럼 순환(circular) 하는 구조로 8개까지의 배열을 다채우고 나면 다시 배열의 처음으로 이동해서 값을 채우는 식으로 계속 순환 한다. ring 구조라고 보면 될것이다. 이 circular buffer 는 매우 구현하기가 쉬우며 특히 queue 를 구현하는데 적합하다. 물론 배열의 크기에 제한이 있을수 있음으로 배열의 크기결정에 신중해야 하며, 아직 읽지 않은 데이타를 덮어쓰지 않도록(한바뀌를 돌았을경우) 안전장치를 마련해두어야 할것이다. 가장 간단하게 구현가능한 안전장치는 데이타를 넣고 읽을때 마다 현재 데이타의 총 갯수를 계산하는 방법일것이다. 그러나 이경우에는 총 갯수를 계산하기 위해서 바쁜 대기 상태에 놓일 가능성이 있음으로 이와 함께 Pthread 의 뮤텍스와 조건변수를 이용하는게 더좋은 방법이 될것이다. 혹은 세마포어를 이용할수도 있을것이다.

이번예제는 (버퍼에)읽기전용 쓰레드와 쓰기전용 쓰레드로 구성된 프로그램을 만들것이다. 그리고 읽기/쓰기 동기화를 위해서 뮤텍스와 조건변수를 사용하게 될것이다. 쓰레드와 뮤텍스 조건변수에 관련된 내용을 이사이트의 다른 문서들을 참고하기 바란다.

이 프로그램은 2개의 파일로 이루어질것이다. 하나는 container 제작을 위한 템플릿 클래스를 가지는 buff.h 다른 하나는 main 함수를 포함하는 main.cc 이다.

예제 : buff.h

#include <string>
#include <pthread.h>

template <typename T>
class array
{
private:
// 저장소의 배열크기는 10으로 고정시킨다.
T container[10];
int member_num, index_num;

pthread_mutex_t mutex;
pthread_cond_t thread_cond;
public:

// 생성자
// mutex 잠금과, 조건변수의 사용을 위한 초기화를 한다.
array()
{
member_num = 0;
index_num = -1;
pthread_mutex_init(&mutex, NULL);
pthread_cond_init(&thread_cond, NULL);
}

// 저장소 container 에 데이타를
// 입력하기 위한 함수
// 만약 10번째 배열까지 다 썼다면
// 다시 0번째 배열부터 쓰기 시작한다.
void push_back(T x)
{
pthread_mutex_lock(&mutex);
cout << "push --> " << member_num%10 << endl;
container[member_num%10] = x;
member_num ++;
pthread_cond_signal(&thread_cond);
pthread_mutex_unlock(&mutex);
}

// 저장소에 있는 데이타를 읽어온다.
// 가장최근의 데이타를 가져오기 위해서
// 참조할 index 값을 가지는 변수 index_num 을
// 사용한다.
// pop() 를 한번 호출하면 index_num 을 1 증가 시켜서
// 다음헤 호출할때는 다음 데이타 값을 가져오도록 한다.
// 만약 저장소에 들어있는 데이타의 크기가 0이라면
// cond_wait 로 시그널을 기다린다.
T pop()
{
T return_container;
pthread_mutex_lock(&mutex);
if (size() == 0)
{
pthread_cond_wait(&thread_cond, &mutex);
}
index_num++;
return_container = container[index_num%10];
pthread_mutex_unlock(&mutex);

return return_container;
}

// 저장소의 크기를 되돌려준다.
int size()
{
return member_num - (index_num + 1);
}
};

예제 : main.cc

#include "arr_tem.h"
#include <string>
#include <stdio.h>
#include <unistd.h>

typedef struct data
{
int data_num;
string name;
} man_info;

array<struct data> buff;

void *push(void *data)
{
int i = 0;
char name[12];
man_info info;

while(1)
{
i++;
info.data_num = i;
sprintf(name, "man_%d", i);
info.name = name;
buff.push_back(info);
if (buff.size() == 10)
{
cout << "대기인원을 초과했습니다. 잠시 기다려주세요." << endl;
sleep(12);
}
else
{
sleep(random()%5);
}
}
}

void *pop(void *data)
{
man_info info;
while(1)
{
info = buff.pop();
cout << "현재 대기 인원 : " << buff.size() << endl;
cout << info.data_num << " : " << info.name << endl;
sleep(3);
}
}

int main()
{
int thr_id;
int a, b, status;
pthread_t p_thread[2];

thr_id = pthread_create(&p_thread[0], NULL, push, (void *)&a);
thr_id = pthread_create(&p_thread[1], NULL, pop, (void *)&b);

pthread_join(p_thread[0], (void **)status);
pthread_join(p_thread[1], (void **)status);
}

컴파일 방법은 다음과 같다.

[root@localhost circular]# g++ -o main main.cc -lpthread

위의 예제는 면접예제이다. 면접을 보기 위해서 피면접자는 대기실에서 기다리고 있어야 한다. 대기실의 정원은 10명이므로 10명이상이 들어오지 못하게 대기실밖에서 관리를 해주어야 한다. 한사람이 면접보는데 걸리는 시간은 3초인 반면 면접보러 오는 사람은 0-5초 간격으로 랜덤하게 도착한다. 그러므로 대기실관리를 해주지 않으면 언젠가는 대기실이 꽉차버릴 것이다. 그걸 막기 위해서 사람을 받아들일때 마다. 대기실의 크기를 조사해서 꽉찼다면 15초 동안은 무조건 진입시키지 않도록 했다. 바쁜 대기를 막기 위해서 size 가 0일경우에는 조건변수로 signal 이 전달될때까지 기다린다.

위의 예제는 circular buffer 를 이용한 queue를 이용해서 구현했다. 지금은 면접자의 정보를 위해서 구조체를 사용하고 있지만 여러분이 마음만 먹는다면 int 형, 클래스, char *형 을 간단히 선언만 함으로써 사용할수 있다.

위의 예제는 queue 가 꽉차면 무조건 12초를 기다리게 되어 있는데, 이왕이면 queue 가 비는 즉시 다음 데이타를 받아들이도록 하는게 좀더 효율적일 것이다. 이것은 역시 조건변수를 이용하면 가능하다. 이것은 간단하니 각자 구현해보도록 한다. 그리고 배열의 크기를 10으로 고정시켜 놨는데, 이왕이면 new 나 malloc 으로 할당할수 있도록 만드는게 좋을것이다. 시간이 남으면 총 면접자를 지정하고 모든 면접자에 대한 면접이 끝나면 종료하도록 수정해 보도록 하자.

이처럼 클래스 템플릿 을 사용하면 다양한 데이타형을 저장할수 있는 container 를 손쉽게 작성할수 있다. 여기에서는 container 를 작성하기 위해서 배열(array)를 사용하고 있는데, STL에서 직접 제공하는 vector, deque 같은 것들을 사용하면 더욱더 유연하게 만들수 있다.


4절. 결론

STL의 최대 사용처는 적당히 효율적인 자료구조를 만들고 만들어진 자료구조에 알고리즘을 적용시키는 작업이다. STL은 이러한 작업을 매우 효율적으로 빠른시간에 끝낼수 있도록 도와준다. 머리에 생각한대로 구현이 가능하다고 보면 될것이다.

보통 이런 자료구조와 알고리즘은 특별한 경우가 아니면 비슷비슷하다. 그러나 비슷함에도 불구하고 거의 매번 자료구조와 알고리즘을 다시 만들어야 할것이다. 자칫 필요 없는 단순 노가다에 시간을 빼앗길수가 있다. STL을 배우고 나면 자료구조와 알고리즘을 설계하고 코딩하는게 즐거워 질것이다.

:::