[ 시퀀스 컨테이너 ]

 

시퀀스 컨테이너느 저장 원소가 삽입 순서에 따라 상대적인 위치(순서)를 갖는 컨테이너 vector, list, deque 입니다. 





vector는 시퀀스 컨테이너이므로 원소의 저장 위치(순서)가  정해지며 배열 기반 컨테이너이므로 원소가 하나의

메모리 블록에 할당됩니다.

 

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
#include <iostream>
#include <vector>
using namespace std;
 
int main( )
    vector<int> v; 
 
    v.push_back(10); 
    v.push_back(20);
    v.push_back(30);
    v.push_back(40);
    v.push_back(50);
 
    /** 
    /* size_type은 원소의 개수나 [] 연산자 등의 index로 사용하는 형식
    /* 이왕이면 컴파일러의 경고 타입을 없애고 보기좋은 코드 작성
    */
    for(vector<int>::size_type i = 0 ; i < v.size() ; ++i)
        cout << v[i] << endl;   
 
    cout << typeid(vector<int>::size_type).name() << endl;    
    
    system("pause");
    return 0;
}

 

 

위의 예제에서 사용되었던 typeid(T)는 T에 대한 typeinfo 객체를 리턴 해줍니다.

 

vector는 크기를 반환하는 세 멤버함수 size(), capacity(), max_size()를 가집니다.

 

size() : 저장 원소의 개수

capacity() : 실제 할당된 메모리 공간의 크기 ( vector만 가지는 멤버함수 )

max_size() : 컨테이너가 담을 수 있는 최대 원소의 개수

 

※ 중요 ※

vector는 원소가 하나의 메모리 블록에 연속(배열 기반 컨테이너)으로 저장 됩니다.

원소가 추가될 때마다 메모리를 재할당하고 이전원소를 모두 복사해야 한다면 너무나 비효율적입니다.

 

이떄 조금이나마 재할당에 드는 성능 문제를 보완 하고자 만들어진 개념이 capacity 입니다.

원소가 추가될 때마다 메모리를 재할당하지 않고 미리 넉넉한 메모리를 확보하면 재할당과 이전 원소를 복사하는 데 드는 비용을 줄일 수 있습니다. 이것은 컨테이너 중 vector만이 가지고 있는 중요한 특징 입니다.

 

 

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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
#include <iostream>
#include <vector>
using namespace std;
 
int main( )
    vector<int> v; 
    vector<int> v_reserve;
 
    cout <<"size: "<< v.size() <<"  capacity: " << v.capacity() <<endl; // 0, 0
    v.push_back(10); // 메모리 재할당과 원소 복사 ( capacity + 이전 capacity / 2 )
    cout <<"size: "<< v.size() <<"  capacity: " << v.capacity() <<endl; // 1, 1
    v.push_back(20); // 메모리 재할당과 원소 복사 ( capacity + 이전 capacity / 2 )
    cout <<"size: "<< v.size() <<"  capacity: " << v.capacity() <<endl; // 2, 2
    v.push_back(30); // 메모리 재할당과 원소 복사 ( capacity + 이전 capacity / 2 )
    cout <<"size: "<< v.size() <<"  capacity: " << v.capacity() <<endl; // 3, 3
    v.push_back(40); // 메모리 재할당과 원소 복사 ( capacity + 이전 capacity / 2 )
    cout <<"size: "<< v.size() <<"  capacity: " << v.capacity() <<endl; // 4, 4                                             
    v.push_back(50); // 메모리 재할당과 원소 복사 ( capacity + 이전 capacity / 2 )
    cout <<"size: "<< v.size() <<"  capacity: " << v.capacity() <<endl; // 5, 6
    v.push_back(60);
    cout <<"size: "<< v.size() <<"  capacity: " << v.capacity() <<endl; // 6, 6 
    v.push_back(70); // 메모리 재할당과 원소 복사 ( capacity + 이전 capacity / 2 )
    cout <<"size: "<< v.size() <<"  capacity: " << v.capacity() <<endl; // 7, 9
    v.push_back(80);
    cout <<"size: "<< v.size() <<"  capacity: " << v.capacity() <<endl; // 8, 9
    v.push_back(90);
    cout <<"size: "<< v.size() <<"  capacity: " << v.capacity() <<endl; // 9, 9
 
    for(vector<int>::size_type i = 0 ; i < v.size() ; ++i)
        cout << v[i] << " ";   
    cout << endl;
    cout << endl;
 
    // reserve 함수 사용
    v_reserve.reserve(8);
    cout <<"size: "<< v_reserve.size() <<"  capacity: " << v_reserve.capacity() <<endl; // 0, 8
    v_reserve.push_back(10); 
    cout <<"size: "<< v_reserve.size() <<"  capacity: " << v_reserve.capacity() <<endl; // 1, 8
    v_reserve.push_back(20); 
    cout <<"size: "<< v_reserve.size() <<"  capacity: " << v_reserve.capacity() <<endl; // 2, 8
    v_reserve.push_back(30); 
    cout <<"size: "<< v_reserve.size() <<"  capacity: " << v_reserve.capacity() <<endl; // 3, 8
    v_reserve.push_back(40); 
    cout <<"size: "<< v_reserve.size() <<"  capacity: " << v_reserve.capacity() <<endl; // 4, 8                                             
    v_reserve.push_back(50); 
    cout <<"size: "<< v_reserve.size() <<"  capacity: " << v_reserve.capacity() <<endl; // 5, 8
    v_reserve.push_back(60);
    cout <<"size: "<< v_reserve.size() <<"  capacity: " << v_reserve.capacity() <<endl; // 6, 8 
    v_reserve.push_back(70); 
    cout <<"size: "<< v_reserve.size() <<"  capacity: " << v_reserve.capacity() <<endl; // 7, 8
    v_reserve.push_back(80);
    cout <<"size: "<< v_reserve.size() <<"  capacity: " << v_reserve.capacity() <<endl; // 8, 8
    v_reserve.push_back(90);// 메모리 재할당과 원소 복사 ( capacity + 이전 capacity / 2 )
    cout <<"size: "<< v_reserve.size() <<"  capacity: " << v_reserve.capacity() <<endl; // 9, 12
 
    for(vector<int>::size_type i = 0 ; i < v_reserve.size() ; ++i)
        cout << v_reserve[i] << " ";   
    cout << endl;
 
    system("pause");    
    return 0;
}

 

위의 예제에서 보면 vector는

 

1. 미리저장할 메모리의 크기(capacity)를 크게 하면 원소가 추가돼도 메모리의 크기가 변하지 않게 됩니다.

2. 원소를 추가하려 할때 메모리의 크기(capacity)가 원소의 개수(size)보다 크지 않다면 메모리 재할당을 수행하게 됩니다.( 같다면 )

 

결국 vector는 이러한 메모리 재할당과 이전 원소 복사 문제가 발생할 수 있습니다. 그래서 vector는 미리 메모리를 할당할 수 있는 메모리 예약 함수 reserve()를 제공합니다. reserve()를 사용하면 미리 메모리를 할당해 capacity를 결정하고 vector에 원소가 추가되더라도 메모리를 재할당 하지 않습니다.

 

 

[ vector 생성자 사이즈 변경및 초기화 ]

1
2
3
4
5
// 기본값 0으로 초기화된 size가 5인 컨테이너
    vector<int> v1(5);        // 0,0,0,0,0    
    // 기본값 10으로 초기화된 size가 5인 컨테이너
    vector<int> v2(5,10);    // 10,10,10,10,10
 

 

[ vector resize함수 사용, 사이즈변경및 초기화 ]

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    vector<int> v3(5); // 이 선언으로 0,0,0,0,0 원소가 있다.
    // 원소가 있으므로, push_back하지 않고 변경해준다.
    v3[0] = 10; 
    v3[1] = 20;
    v3[2] = 30;
    v3[3] = 40;
    v3[4] = 50;    
 
    // resize함수로 초기값이 0인 size가 10인 컨테이너 생성
    v3.resize(10);// 10,20,30,40,50,0,0,0,0,0
 
    // 사이즈는 줄었지만, capacity는 변경없음( size : 5,  capacity : 10 )
    v3.resize(5); // 10,20,30,40,50    
    // resize(10, 100 ) : size를 10으로 확장하고 추가되는 원소를 100으로 초기화

 

[ vector의 clear()함수와 empty() 함수 ]

 

1
2
3
4
5
6
7
8
9
10
11
    v3.clear(); // size : 0 capacity : 5
    // ※clear한다고 해서 메모리(capacity)가 제거되지 않음
    if( v3.empty() ){
        cout<<"비었다"<<endl;
    }
 
    /**
    * 메모리(capacity)를 완전히 제거하는방법
    */    
    //임시객체 기본생성자로 만든vector컨테어나와 v3 swap한다
    vector<int>().swap(v3); // size : 0, capacity : 0

 

[ swap함수를 이용하여 원소교환 ]

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
vector<int> v4;
    v4.push_back(10);
    v4.push_back(20);
    v4.push_back(30);
    vector<int> v5;
    v5.push_back(100);
    v5.push_back(200);
    v5.push_back(300);
    v4.swap(v5);
 
    //v4의 원소들과 v5원소들이 교환됨
    for(vector<int>::size_type i = 0; i < v4.size(); ++i){
        cout<< v4[i] << "," << v5[i] <<endl;
    }
    cout<<endl;



[ front(), back() 함수 ]

 

1
2
3
4
5
6
7
8
9
10
cout<< v4[0] << "," << v4.front() <<endl; // v4[0]은 v4.at(0)과 같음
    cout<< v4[2] << "," << v4.back() <<endl;
 
    // 원소 수정    
    v4.front() = 10;    
    v4.at(1)   = 20;
    v4.back()  = 30;    
    cout<< v4[0] << "," << v4.front() <<endl;
    cout<< v4[1] << "," << v4.at(1) <<endl;
    cout<< v4[2] << "," << v4.back() <<endl;

 

 

vector와 deque은 일반 배열처럼 임의 취치의 원소를 참조하는 두 인터페이스르 제공합니다.

 

[] 연산자 :  범위 점검을 하지 않아 속도가 빠르며

at(index) 멤버함수 : 범위 점검을 해서 안전함, 범위가 넘어가면 out_of_range 예외 발생

 

 

[ assign() 멤버 함수 ]

 

1
2
    vector<int> v6(5, 1); // 초기값1인 5개의 원소를 갖는 컨테이너 생성
    v6.assign(5, 10); // 5개의 원소값을 2로 할당(n개의 원소에 x의값을 할당합니다.)

 

 

[ begin(), end() 멤버 함수, 상수 반복자 ]

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
    vector<int>::iterator Iter;
    vector<int>::const_iterator const_Iter;
 
    for( Iter = v6.begin(); Iter != v6.end(); ++Iter ){
        cout<< *Iter << endl;
    }
    cout<<endl;
 
    const_Iter = Iter = v6.begin();
    cout<< *(++Iter) <<endl;
    cout<< *(++const_Iter) <<endl;
 
    *Iter = 40;
//    *const_Iter = 50; 상수반복자는 원소변경 못함( const int* )

 

 

[ const키워드와 반복자 ]

 

1
2
3
4
vector<int>::iterator Iter; //다음원소 이동가능, 원소변경 가능
vector<int>::const_iterator const_Iter;//다음원소 이동가능, 원소변경 불가능
const vector<int>::iterator Iter_const;//다음원소 이동불가능, 원소변경 가능
const vector<int>::const_iterator const_Iter_const; //둘다 불가능
 

반대로 동작하는 역방향 반복자( reverse_Iterator )

reverse_iterator는 vector에 반복자 어댑터로 typedef되어 있습니다.

 

 

[ insert() 멤버함수 ]

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
    vector<int>::iterator Iter_v6 = v6.begin()+2; //10,20,30,40,50에서 30을 가르킴    
 
    // Iter_v6가 가르키는 위치에 100을 삽입하고 삽입한 100의 위치를 가르키는 반복자
    // 를 리턴하여 Iter_insertPos에 대입한다.
    vector<int>::iterator Iter_InsertPos = v6.insert(Iter_v6, 100);
    
 
    // Iter_InsertPos가 가리키는 위치에 정수 200을 3개 삽입
    v6.insert(Iter_InsertPos, 3, 200 );
 
 
    // Iter_InsertPos가 가리키는 위치에 v4의 구간 [b,e)의 원소 삽입
    vector<int> v7;
    v7.push_back(100);
    v7.push_back(200);
    v7.push_back(300);
    Iter_v6 = v6.begin(); //10을 가르킴
    v6.insert(Iter_v6, v7.begin(), v7.end() );
    
    for( Iter_v6 = v6.begin(); Iter_v6 != v6.end(); ++Iter_v6 ) {
        cout<< *Iter_v6 <<endl;
    }

 

[ erase() 멤버함수 ]

 

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
35
36
37
38
39
40
#include <iostream>
#include <vector>
 
using namespace std;
 
int main(){
    
    /**
    * erase() 멤버함수
    */
 
    vector<int> v;
    v.push_back(10);
    v.push_back(20);
    v.push_back(30);
    v.push_back(40);
    v.push_back(50);
 
    vector<int>::const_iterator Iter = v.begin()+1;
    //Iter가 가르키는 위치의 원소 20 을 제거합니다. 제거된 원소의 다음원소를 리턴
    //리턴된 반복자를 erase_Iter에 대입합니다. 30
    vector<int>::const_iterator erase_Iter = v.erase( Iter );
 
    for( Iter = v.begin(); Iter != v.end(); ++Iter ){
        cout<< *Iter << endl;
    }
    cout<<endl;
    cout<<"제거후 리턴된 반복자가 가르키는 값 : "<< *erase_Iter <<endl;
    cout<<endl;
 
    // 구간 [b,e) 사이의 모든 원소를 지운다. 10만 남겨놓고 30,40,50 지움
    // erase()멤버함수에서 리턴되는 반복자는 NULL을 가르키게된다.v.end()
    v.erase(v.begin()+1, v.end());
    for( Iter = v.begin(); Iter != v.end(); ++Iter ){
        cout<< *Iter << endl;
    }    
 
    system("pause");
    return 0;
}

 

[ 반복자로 동작하는 생성자와 assign() 멤버함수 ]

 

1
2
3
vector<int> v2(v.begin(), v.end()); // 순차열[b,e) 생성자로 초기화
vector<int> v3;
v3.assign(v2.begin(), v2.end()); //순차열 [b,e) v3에 할당

 

vector의 생성자는 반복자를 통해서 초기화될 수 있으며 assign() 멤버 함수도 반복자를 통해 할당될 수 있습니다.

 

 

[ vector와 vector의 비교 컨테이너 연산자 ]

 

컨테이너 연산자 : ==, !=, <, <=, >, >=

 

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>
using namespace std;
 
int main(){
 
    vector<int> v1;
    v1.push_back(10);
    v1.push_back(20);
    v1.push_back(30);
    v1.push_back(40);
    v1.push_back(50);
    vector<int> v2;
    v2.push_back(10);
    v2.push_back(20);
    v2.push_back(50);
 
    if( v1 == v2 ){ //모든원소가 같다면
        cout<<"v1 == v2"<<endl;
    }
    if( v1 != v2 ){ //모든원소가 같지 않다면
        cout<<"v1 != v2"<<endl;
    }
 
    // 순차열의 원소를 하나씩 순서대로 비교하여
    // v2의 원소가 크다면 참 아니면 거짓
    // 10 == 10, 20 == 20, 30 < 50 여기서 v2가 큼
    if( v1 < v2 ){ 
        cout<<"v1 < v2"<<endl;
    }
 
    system("pause");
    return 0;
}


 

1. 컨테이너

 

컨테이너는 같은 타입을 저장, 관리할 목적으로 만들어진 클래스 입니다.

 

▣ 표준 시퀀스 컨테이너 ( Standard Sequence Container )

 - 컨테이너 원소가 자신만의 삽입 위치(순서)를 가지는 컨테이너 ( vector, list, deque : 선형 구조 )

 - 삽입되는 순서에 따라 원소의 위치가 결정되고 바뀌지 않습니다.

 - 컨테이너 끝에 데이터를 추가하고 제거하기 위한 push_back()과 pop_back() 멤버함수를 가집니다.

 

▣ 표준 연관 컨테이너 ( Standard Associative Container )

 - 저장 원소가 삽입 순서와 다르게 특정 정렬 기준에 의해 자동 정렬되는 컨테이너 ( set, multiset, map, multimap : 비선형 구조 )

 - 삽입 순서와 상관없이 정렬 기준 ( default : less )에 따라 원소의 위치가 결정 됩니다.

 

 

컨테이너는 데이터를 하나의 연속된 메모리 단위로 저장하느냐에 따라 두가지로 나눕니다.

 

▣ 배열 기반 컨테이너( Array-based Container ) : 데이터 여러개가 하나의 메모리 단위에 저장됩니다.

 - vector, deque

 - operator[] 연산자를 이용해 일반 배열처럼 컨테이너 원소에 접근할 수 있습니다.

 

▣ 노드 기반 컨테이너( Node-based Container ) : 데이터 하나를 하나의 메모리 단위에 저장합니다.

 - list, set, multiset, map, multimap

 

모든 컨테이어는 원소의 개수를 반환하는 size() 멤버 함수를 가집니다.

 

 

--------------------------------------------------------------------------------------------------------------------------

2. 반복자

 

반복자는 컨테이너에 저장된 원소를 순회하고 접근하는 일반화된 방법을 제공합니다.

반복자는 컨테이너와 알고리즘이 하나로 동작하게 묶어주는 인터페이스 역할을 합니다.

특정 컨테이너에 종속적이지 않고 독립적이면서도 언제든지 컨테이너와 결합하여 동작할 수 있습니다.

 

반복자의 특징

 - 컨테이너 내부의 원(객체)를 가리키고 접근할 수 있어야 합니다. ( * 연산자 제공 )

 - 다음원소로 이동하고 컨테이너의 모든 원소를 순회할 수 있어야 합니다. ( ++,!=,== 연산자 제공 )

 

 

STL에서 컨테어너 원소(객체)의 집합을 순처열( Sequence )이라 하고, 순차열은 하나의 시작과 하나의 끝을 갖습니다. 여기서 반복자는 순차열의 한 원소를 가리킵니다.

 

STL의 모든 컨테이너는 자신만의 반복자를 갖습니다. 멤버함수 begin()과 end()가 순차열의 시작과 끝을 가리키는 반복자를 반환합니다.

 

 

반복자                                                   반복자

begin()                                                  end()

 

A            B           C          D           E        n

                           

                         Iterator

 

 

※ 순차열 [begin, end) 구간의 원소는 A,B,C,D,E, [begin, iter) 구간의 원소는 A,B [iter, end) 구간의 원소는 C,D,E 입니다.

    만일 순차열[p, q)에서 p,q가 가리키는 원소가 같다면 이 순차열은 원소가 없습니다.

 

◎ 입력 반복자( Input Iterator ) : 현 위치의 원소를 한 번만 읽을 수 있는 반복자

◎ 출력 반복자( Output Iterator ) : 현 위치의 원소를 한 번만 쓸 수 있는 반복자

◎ 순반향 반복자( Forward Iterator ) : 입력, 출력 반복자 기능에 순방향으로 이동(++)이 가능한 재할당될 수 있는 반복자

◎ 양방향 반복자( Bidirectional Iterator ) : 순방향 반복자 기능에 역방향으로 이동(--)이 가능한 반복자

 - list, set, multiset, map, multimap

◎ 임의 접근 반복자( Random Access Iterator ) : 양방향 반복자 기능에 +,-,+=,-=,[] 연산이 가능한 반복자

 - vector, deque

 

모든 컨테이너는 양방향 반복자 이상을 제공 합니다.

 

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>
using namespace std;
 
int main( )
{
    vector<int> v; 
 
    v.push_back(10); 
    v.push_back(20);
    v.push_back(30);
    v.push_back(40);
    v.push_back(50);
 
    vector<int>::iterator iter=v.begin(); //시작 원소를 가리키는 반복자
    cout << iter[0] << endl; // [] 연산자
    cout << iter[1] << endl;
    cout << iter[2] << endl;
    cout << iter[3] << endl;
    cout << iter[4] << endl;
    cout << endl;
 
    iter += 2; // += 연산
    cout << *iter << endl;        // 30
    cout << endl;
 
    vector<int>::iterator iter2 = iter+2; // + 연산
    cout << *iter2 << endl;        // 50
 
    system("pause");
    return 0;
}

 

 

--------------------------------------------------------------------------------------------------------------------------

3. 알고리즘

 

STL은 순차열의 원소를 조사, 변경, 관리, 처리할 목적으로 알고리즘이라는 구성요소를 제공합니다.

알고리즘은 한 쌍의 반복자([begin, end))을 필요로 하며 알고리즘 대부분은 순방향 반복자를 요구하지만, 몇몇 알고리즘은 임의 접근 반복자를 요구 합니다.

 

 

[알고리즘 일곱 가지의 범주로 분류]

 

◇ 원소를 수정하지 않는 알고리즘

◇ 원소를 수정하는 알고리즘

◇ 제거 알고리즘

◇ 변경 알고리즘

◇ 정렬 알고리즘

◇ 정렬된 범위 알고리즘

◇ 수치 알고리즘

 

 

find 알고리즘 : 순방향 반복자를 요구, 순방향 반복자만 지원하는 컨테이너(순차열)이라면, 어떤 컨테이너가 와도 알고리즘 수행가능

 

1
2
3
4
5
6
7
8
9
10
#include <algorithm> // find 사용
    
vector<int>::iterator iter;
iter = find(v.begin(), v.end(), 20); //[begin, end)에서 20 찾기
cout << *iter << endl;
 
iter = find(v.begin(), v.end(), 100); //[begin, end)에서 100 찾기
if( iter == v.end() ) // 100이 없으면 iter==v.end() 임
    cout << "100이 없음!" << endl;
        

 

 

sort 알고리즘 : 순차열을 정렬, 임의 접근 반복자를 요구, vector와 deque는 sort알고리즘을 수행할 수 있지만, 다른컨테이너는 불가능합니다. 연관컨테이너는 컨테이너만의 정렬 기준을 가지고 있습니다.

 

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
#include <vector>
#include <list>
#include <algorithm>
using namespace std;
 
vector<int> v; 
v.push_back(50); 
v.push_back(40);
v.push_back(30);
v.push_back(20);
v.push_back(10);
 
list<int> lt;
lt.push_back(10); 
lt.push_back(20);
lt.push_back(30);
lt.push_back(40);
lt.push_back(50);
 
sort(v.begin(), v.end()); // 정렬 가능(vector는 임의 접근 반복자)
                          // 디폴트 less, 오름차순 정렬
//sort(lt.begin(), lt.end()); // 에러!(list는 양방향 반복자)
 
for( vector<int>::iterator Iter = v.begin(); Iter != v.end(); ++Iter ){
       cout<< *Iter <<endl;
}
 

 

 

--------------------------------------------------------------------------------------------------------------------------

4. 함수객체

 

STL 에서 함수 객체는 클라이언트가 정의한 동작을 다른 구성 요소에 반영하려 할 때 사용 됩니다.

STL 알고리즘이 함수 객체, 함수, 함수 포인터 등의 함수류를 인자로 받아 알고리즘을 유연하게 동작 시킵니다.

 

 

1
2
3
 
sort(v.begin(), v.end(), less<int>() );    // 오름차순 정렬
sort(v.begin(), v.end(), greater<int>() ); // 내림차순 정렬

 

 

--------------------------------------------------------------------------------------------------------------------------

5. 어댑터

 

구성 요소의 인터페이스를 변경합니다.

 

[ 어댑터 종류 ]

 

◇ 컨테이너 어댑터 ( Container Adaptor ) : stack, queue, priority_queue

 - stack 컨테이너 어댑터는 일반 컨테이너를 LIFO 방식의 스택 컨테이너로 변환합니다.

empty, size, push_back, pop_back, back 인터페이스(멤버함수)를 지원하는 컨테이너는 모두 LIFO방식의 스택으로 변환할 수 있습니다.

시퀀스 컨테이너는 모두 멤버함수를 가지므로 stack 컨테이너 어댑터의 컨테이너로 이용할 수 있습니다.

stack 컨테이너 어댑터의 디폴트 컨테이너는 deque 컨테이너 입니다.

 

[stack 컨테이너]

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
#include <iostream>
#include <stack>
using namespace std;
 
int main(){
 
    stack<int> s;
    s.push(10);
    s.push(20);
    s.push(30);
 
 
    cout<< s.top() << endl;    //30
    s.pop();
    cout<< s.top() << endl; //20
    s.pop();
    cout<< s.top() << endl; //10
    cout<< s.top() << endl; //10
    s.pop();
 
    if( s.empty() ){
        cout<<"empty"<<endl;
    }
 
    system("pause");
    return 0;
}

 

 

[vector 컨테이너를 적용한 stack 컨테이너]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <vector>
#include <stack>
using namespace std;
 
void main( )
{
    stack<int, vector<int> > st; // vector 컨테이너를 이용한 stack 컨테이너 생성
 
    st.push( 10 ); // 데이터 추가(입력)
    st.push( 20 );
    st.push( 30 );
 
    cout << st.top() << endl; // top 데이터 출력
    st.pop(); // top 데이터 삭제
    cout << st.top() << endl;
    st.pop();
    cout << st.top() << endl;
    st.pop();
 
    if( st.empty() ) // 스택이 비었는지 확인
        cout << "stack이 데이터 없음" << endl;
}

 

 

◇ 반복자 어댑터 ( Iterator Adaptor ) : reverse_iterator, back_insert_iterator, front_insert_iterator, insert_iterator

 - reverse_iterator는 일반 반복자의 동작 방식을 반대로 동작시키는 역방향 반복자(reverse_iterator)로 변환합니다.

 - 역방향 반복자는 반복자가 가리키는 원소와 실제 가리키(참조)는 값이 다르다는 점입니다. 반복자가 가리키는 원소 다음 원소의 값을 참조 합니다. 이렇게 설계한 이유는 알고리즘 수행 시 정방향 반복자와 호환되도록 하기 위해서 입니다.

 

1
2
3
4
5
6
//일반 반복자 iterator를 역방향 반복자 reverse_iterator로 변환
reverse_iterator< vector<int>::iterator > riter(v.end());
reverse_iterator< vector<int>::iterator > end_riter(v.begin());
 
for(   ; riter != end_riter ; ++riter )
    cout << *riter << " ";

 

        begin()                                                       end()

          10           20         30          40          50          n

        end_riter( 10을 가르키고, 10앞의 값을 참조)      riter( n을 가르키고 50의 값을 참조 )

 

위의 설계의 장점은

--연산자를 사용하지 않고 ++ 연산자만으로 정방향, 역방향의 순회가 모두 가능 하다는 점입니다. 이는 대부분의 알고리즘이 ++연산자만 으로 구현 되어 있으며 이렇게 이미 구현된 알고리즘에 정방향과 역방향 순회가 모두 가능하게 합니다.

 

정방향 순회시 순차열 구간의 원소는 반복자 이전까지를 나타내는 것을 생각해보면 역방향시 반복자가 가르키는 원소의 앞의 값을 참조하는것이 역방향으로 순회시 ++연산자로 순회를 가능하게 해주는 것 입니다.

 

1
2
3
4
5
// STL 모든 컨테이너는 반복자 어댑터 reverse_iterator를 typedef 타입으로 정의하며
// rbegin(), rend()로 컨테이너의 역방향 반복자를 반환함.
vector<int>::reverse_iterator riter(v.rbegin()); 
for(   ; riter != v.rend() ; ++riter )
    cout << *riter << " ";

 

 

 

◇ 함수 어댑터 ( Function Adaptor ) : 바인더( binder ), 부정자( negator ), 함수 포인터 어댑터( adaptor for pointers to functions )

 - not2는 조건자 함수 객체(이항)를 NOT(반대)로 변환합니다. ( 조건자는 bool타입을 반환하는 함수류 입니다 )

 - not1은 단항 조건자에 사용되며, not2는 이항 조건자에 사용 됩니다.

 

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 <functional> //not2 사용
using namespace std;
 
int main( )
{
    //임시 less 객체로 비교
    cout << less<int>()(10, 20) << endl; // 1
    cout << less<int>()(20, 20) << endl; // 0
    cout << less<int>()(20, 10) << endl; // 0
    cout << "==============" <<endl;
    // 임시 객체 less에 not2 어댑터 적용
    cout << not2( less<int>() )(10, 20) << endl; // 0
    cout << not2( less<int>() )(20, 20) << endl; // 1    
    cout << not2( less<int>() )(20, 10) << endl; // 1
    cout << endl;
 
    /**
    * not2( less<int>() ) 이 구문은
    * less함수객체가 < 연산이므로 반대 의미가 있는 >=연산 기능의 함수객체를 반환하게 됩니다.
    */
 
    less<int> l;
    cout << l(10, 20) << endl; // less 객체 l로 비교    
    cout << l(20, 20) << endl;    
    cout << l(20, 10) << endl;
    cout << "==============" <<endl;
    cout << not2( l )(10, 20) << endl; // less 객체 l에 not2 어댑터 적용
    cout << not2( l )(20, 20) << endl;
    cout << not2( l )(20, 10) << endl;
 
    system("pause");
    return 0;
}

 

 

--------------------------------------------------------------------------------------------------------------------------

6. 할당기

 

컨테이너의 메모리 할당 정보와 정책(메모리 할당 모델)을 캡슐화한 STL 구성 요소입니다.

할당기는 템플릿 클래스이며, 모든 컨테이너는 기본 할당기를 사용합니다.

 

사용자 정의 할당기는 사용자가 직접 메모리 할당 방식을 제어할 수 있게 합니다.

 

모든컨테이너는 템플릿 매개변수에 할당기를 인자로 받습니다. 기본할당기는 allocator<T>이며, 컨테이너는 템플릿 매개변수에 디폴트 매개변수 값으로 기본 할당기를 지정합니다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <iostream>
#include <vector>
#include <set>
#include <map>
using namespace std;
 
void main( )
{
    //vector<typename T, typename Alloc = allocator<T> >
    // vector<int> 와 같음
    vector< int, allocator<int> > v;
    v.push_back( 10 );
    cout << *v.begin() << endl;
 
    //set<typename T, typename Pred = less< T >, typename Alloc = allocator<T> >
    // set<int> 와 같음
    set< int, less<int>, allocator<int> > s;
    s.insert( 10 );
    cout << *s.begin() << endl;
}

 

 


STL은 표준 C++ 라이브러리의 일부분으로 Standard Template Library의 약자입니다.

STL은 프로그램에 필요한 자료구조와 알고리즘을 템플릿으로 제공하는 라이브러리입니다.

자료구조와 알고리즘은 서로 반복자라는 구성요소를 통해 연결합니다.

 

[STL의 구성요소]

 

컨테이너( Container ) : 객체를 저장하는 객체로 컬렉션 혹은 자료구조라고도 합니다.

 

반복자( Iterator ) : 포인터와 비슷한 개념으로 컨테이너의 원소를 가리키고, 가리키는원소에 접근하여 다음원소를 가리키게하는 기능을 합니다.

 

알고리즘( Algorithm ) : 정렬, 삭제, 검색, 연산 등을 해결하는 일반화된 방법을 제공하는 함수 템플릿입니다.

 

함수객체( Function Object ) : 함수처럼 동작하는 객체로 operator() 연산자를 오버로딩한 객체입니다. 컨테이너와 알고리즘 등에 클라이언트 정책을 반영하게 합니다.


어댑터( Adaptor ) : 구성 요소의 인터페이스를 변경해 새로운 인터페이스를 갖는 구성요소로 변경합니다(새로운 구성 요소처럼 보입니다)

 

할당기( Allocator ) : 컨테이너의 메모리 할당 정책을 캡슐화한 클래스 객체로 모든 컨테이너는 자신만의 기본 할당기를 가지고 있습니다.

 

 

[STL의 3가지 특징]

효율성, 일반화 프로그램( 재사용성), 확장성

 

STL알고리즘은 너무나도 일반적입니다. 특정한 자료구조와 형식에 종속저이지 않고, 효율성도 다른 라이브러리에 뒤지지않을 뿐만아니라 사용자가 작성한 구성 요소와도 잘 동작하며, 확장성도 뛰어납니다.

 

 

 


C++에서는 함수로부터 객체를 전달받거나 함수에 객체를 전달할 때 '값에 의한 전달' 방식을 사용합니다.

함수 매개변수는 실제 인자의 사본을 통해 초기화되며, 어떤 함수를 호출한 쪽은 그함수가 반환한 값의 사본을 돌려 받습니다.

 

위의 내용처럼 사본을 만들어내는 원천이 바로 복사 생성자 입니다.

 

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
class Person 
public : 
    Person(); 
    virtual ~Person(); 
 
private
    std::string name; 
    std::string address; 
}; 
 
class Student : public Person 
public : 
    Student(); 
    ~Student();
 
private : 
    std::string schoolName; 
    std::string schoolAddress; 
}; 
 
bool ValidateStudent( Student s ); 
 
Student plato;
 
bool platoIsValid = ValidateStudent( plato );

 

 

plato로부터 매개변수 s를 초기화시키기 위해 Student의 복사 생성자가 호출된다. s는 ValidateStudent가 복귀할 때 소멸된다. Student 객체가 생성될 때마다 string 멤버도 생성된다. 게다가 Student 객체는 Person 객체로부터 파생되었기 때문에, Student 객체의 생성자가 호출되기 이전에 Person 객체가 생성된다. Person 객체가 생성될 때에도 string 멤버도 생성된다.

이처럼 Student 객체를 값으로 전달하는 데 날아간 비용을 보면, 생성자 6번 소멸자 6번입니다.
 
위와 같은 방식의 문제점을 해결하기 위해서는?

 

bool ValidateStudent( const Student& s ); //참조의 의한 전달 방식으로 바꾸기만 하면 됩니다. 

 

이렇게 할 경우,

 

우선 새로 만들어지는 객체라는 것이 없으므로 생성자/소멸자가 호출되지 않는다.

그리고 const를 붙임으로써 전달인자로 보낸 객체가 함수에 의해 변하지 않는다는 것을 명시해준다. 원래 값에 의한 전달 방식때도 원본 객체는 보호되는데 그 의미를 그대로 적용한 것이라고 보면 됩니다.

 

또한, '복사 손실' 문제도 없어집니다. 아래의 코드를 봅시다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Window 
public : 
    std::string GetName() const
    virtual void Display() const
}; 
 
class WindowWithScrollBars : public Window 
public :  
    .... 
    virtual void Display() const
}; 
 
void PrintNameAndDisplay( Window w ) 
    std::cout << w.GetName(); 
    w.Display(); 
 
WindowWithScrollBars wwsb;
 
PrintNameAndDisplay( wwsb );
 
 
위의 코드는 wwsb 객체가 PrintNameAndDisplay() 함수의 인자로 전달될때 값의 의한 전달을 하게되는데, 이렇게 되면 WindowWithScrollBars 객체의 구실을 할 수 있는 부속 정보가 전부 싹뚝 잘립니다.
PrintNameAndDisplay() 함수안에 w는 어떤 타입의 객체가 넘겨지든 아랑곳없이 Window클래스 객체의 면모만을 갖게 됩니다. 그래서
PrintNameAndDisplay() 함수내에서 w.Display(); 는 Window::Display() 일것니다.
 
즉! 이런 '복사 손실' 을 방지하려면
void PrintNameAndDisplay( const Window& w );
 
참조의 의한 전달로 바꾸시면 됩니다.
 

참조자는 보통 포인터를 써서 구현됩니다. 즉, 참조자를 전달한다는 것은 결국 포인터를 전달한다는 의미인데,

전달하는 객체 타입이 기본제공 타입(int, char 등) 이라면 참조자보다는 값에 의한 전달을 하는 쪽이 더 효율적일 때가 많다.

 

기본제공 타입은 대개 크기가 작은 편이다.

그러나! 타입 크기가 작다고 해서 전부 값에 의한 전달이 효율적이지는 않다.

한개의 포인터 멤버만 가지고 있는 객체가 있다면(STL 컨테이너가 대표적) 이 객체를 복사할 때 포인터가 가리키는 대상까지 복사하는 작업도 따라다녀야 해서 오히려 비용이 더 들 수 있게 된다.

 

객체 크기도 작고 복사 생성자도 간단하게 만들어졌다 해도, 일부 컴파일러에서 기본제공 타입과 사용자 정의 타입을 다르게 취급할 수 있기 때문에(레지스터에 들어가는지 안들어가는지의 차이가 존재한다) 수행 성능에 있어서 차이를 보일 수 있다.

 

또한 사용자 정의 타입은 크기 변화에 언제든 노출이 되어있다. 지속적으로 추가 될 수 있는 부분이 있다는 말입니다.

 

이것 만은 잊지 말자!

◆ '값의 의한 전달' 보다는 '상수 객체 참조자에 의한 전달'을 선호 합시다. 대체적으로 효율적일 뿐만 아니라 복사 손실 문제까지 막아 줍니다.

◆ 이번 항목에서 다룬 법칙은 기본제공 타입 및 STL 반복자, 그리고 함수 객체 타입에는 맞지 않습니다. 이들에 대해서는 '값에 의한 전달'이 더 적절합니다.



C++에서 새로운 클래스를 정의 한다는 것은 새로운 타입을 하나 정의하는 것과 같습니다. 

좋은 타입이란?

1) 문법이 자연스러움

2) 의미구조가 직관적

3) 효율적인 구현이 한가지 이상 가능해야 합니다.

 

클래스 설계시 신경써야 하는 것?

 

1) 새로 정의한 타입의 객체 생성 및 소멸은 어떻게 이루어져야 하는가?

메모리 할당 함수(operator new, operator delete)들을 직접 작성시 생성자, 소멸자에 대한 설계에 영향을 미치게 됩니다.

 

2) 객체 초기화는 객체 대입과 어떻게 달라야 하는가?

초기화와 대입을 확실히 헷갈리지 않는 거이 가장 중요합니다.

 

3) 새로운 타입으로 만든 객체가 값에 의해 전달되는 경우에 어떤 의미를 줄 것인가?

어떤 타입에 대해 '값에 의한 전달'을 구현하는 쪽은 복사 생성자입니다.

 

4) 새로운 타입이 가질 수 있는 적법한 값에 대한 제약은 무엇으로 잡을 것인가? 

  

5) 기존의 클래스 상속 계통망에 맞출 것인가?

멤버 함수가 가상인가 비가상인가의 여부가 상속에 있어서 큰 요인이 됩니다.  다른 클래스들이 상속할 수 있게 만들자고 결정했다면, 이에 따라 멤버 함수의 가상 함수 여부가 결정됩니다. 특히 소멸자가 그렇습니다.

 

6) 어떤 종류의 타입 변환을 허용할 것인가?

암시적 : T1 -> T2 타입 변환시 T1 클래스에 타입변환 함수를 넣어두던가, 인자 1개로 호출되는 non-explicit 생성자를 T2에 넣는다.

명시적 : 해당 변환을 맡는 별도 이름의 함수를 만들되 위에서 언급한 것들은 만들지 않는다.

 

7) 어떤 연산자와 함수를 두어야 의미가 있을까? 

 

8) 표준 함수들 중 어떤 것을 허용하지 말 것인가? 

허용되지 않는 것은 private 멤버가 될 것이다.

 

9) 새로운 타입의 멤버에 대한 접근권한을 어느쪽에 줄 것인가? 

private, protected, public 영역 및 프렌드 등에 대한 고민이다.

 

10) 선언되지 않은 인터페이스로 무엇을 둘 것인가? 

보장할 수 있는 부분은 수행 성능 및 예외 안전성, 그리고 자원 사용이다. 보장한다고 결정하면 클래스 구현에 있어 제약으로 작용한다. 

 

11) 새로 만드는 타입이 얼마나 일반적인가? 

 

12) 정말로 꼭 필요한 타입인가? 

기존 클래스에 기능 몇개가 아쉬워 파생 클래스를 새로 정의한다면, 간단하게 비멤버 함수나 템플릿을 정의하는 것이 낫다.

 

이것 만은 잊지 말자!

◆ 클래스 설계는 타입 설계입니다. 새로운 타입을 정의하기 전에, 이번 항목에 나온 모든 고려사항을 빠짐없이 점검해 보십시오.




어떤 인터페이스를 어떻게 써 봤는데 결과 코드가 사용자가 생각한 대로 동작하지 않는다면, 그 코드는 컴파일 되지 않아야 맞습니다. 거꾸로 생각해서, 어떤 코드가 컴파일 된다면 그 코드는 사용자가 원하는 대로 동작해야할 것입니다.

 

'제대로 쓰기에 쉽고, 엉터리로 쓰기에 어려운' 인터페이스를 개발 하려면?

우선 사용자가 저지를 만한 실수의 종류를 머리에 넣어 두고 있어야 합니다.  

 

1
2
3
4
5
6
7
8
9
10
11
12
class Date
 
{
 
public :
 
    Date( int month, int day, int year );
 
}; 
 
Date d( 30, 3, 1995 ); // 3, 30 이어야 하는데 30, 3을 넣음.
Date d( 3, 40, 1995 ); // 3, 30 이어야 하는데 3, 40을 넣음. 

 

위의 코드는 어찌보면, 문제될게 없어보이지만, 사용자가 쉽게 실수를 저지를수 있습니다.

위의 주석대로 매개변수의 전달 순서가 잘못될 여지가 있습니다.

 

위의 문제는 간단한 랩퍼 타입을 각각 일, 월, 연을 만들고 이 타입을 Date 생성자 안에 두면, 어느정도 해결 됩니다.

 

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
struct Day 
    explicit Day( int d ) 
        : day( d ) 
    {} 
    int day; 
}; 
 
struct Month 
    explicit Month( int m ) 
        : month( m ) 
    {} 
    int month; 
};
 
struct Year 
    explicit Year( int y ) 
        : year( y ) 
    {} 
    int year;
}; 
 
class Date 
public : 
    Date( const Month& m, const Day& m, const Year& y ); 
};
 
Date d( 30, 3, 1995 );                   // 에러. 타입이 틀림.
Date d( Day(30), Month(3), Year(1995) ); // 에러. 타입이 틀림. 
Date d( Month(3), Day(30), Year(1995) ); // OK

 

위와 같이 적절한 타입만 제대로 준비되어 있으면, 각타입의 값에 제약을 가하더라도 괜찮은 경우가 생기게 됩니다.

 

예를 들어,

월이 가질 수 잇는 유효한 값은 12개이므로, Month 구조체 타입에 이 사실을 제약조건으로 부여할 수 있다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Month
public : 
    static Month Jan() { return Month( 1 );  } 
 
    static Month Feb() { return Month( 2 );  } 
 
    ...  
 
    static Month Dec() { return Month( 12 ); } 
 
private : 
    explicit Month( int m ); // Month 값이 새로 생성되지 않도록 명시호출 생성자가 private 멤버이다. 
 
    ... 
 
};  
 
Date d( Month::Jan(), Day(30), Year(1995) );

 

 

사용자 실수로 예상되는 것중에 또 하나는 다음과 같은 경우가 있다

 

if( a * b = c ) ... // '='가 '=='가 되어야 맞다

 
우리는 항목 3에 잘 설명해논, operator*의 반환 타입을 const로 한정함으로써 사용자가 사용자정의 타입에 대해 위와같은 실수를 저지르지 않도록 할 수 있었습니다.

 

 

기본적으로 우리는 int등의 타입이 어떻게 동작하는지 그 성질을 이미 다 알고 있기 때문에, 사용자를 위해 만드는 타입도 웬만하면 이들과 똑같이 동작하게 만드는 센스를 갖추어야 합니다.

 

이것은 일관성 있는 인터페이스를 제공하기 위해서입니다.

STL 컨테이너의 인터페이스는 전반적으로 완벽하진 않지만, 일관성을 갖고 있으며, 이때문에 사용하는데 큰 부담이 없습니다.

ex ) STL컨테이너 size() 멤버함수를 개방해 놓음

 

사용자 쪽에서는 뭔가를 외워야 제대로 쓸 수 있는 인터페이스는 잘못 쓰기 쉽습니다.

 

예를 들어,

이전 항목 13 에서 봤던 createInvestment() 팩토리 함수의 경우, 클래스 포인터를 반환한다.

함수에서 얻어낸 포인터는 동적 할당된 자원이기 때문에, 종료 전에 반드시 메모리를 해제해야한다.

사용자는 다음과 같은 실수를 저지를 수 있습니다.

 

1) 포인터 삭제를 잊어버렸다!

2) 같은 자원에 delete를 두번 해버렸다!!

 

이전 항목 13 에서는 이 문제를 해결하기 위해 스마트 포인터를 이용했다.

그러나 사용자는 스마트 포인터를 사용해야 한다는 사실을 모르면 어떻게 할까요?

 

그 해결책,

애초부터 팩토리 함수가 스마트 포인터를 반환하게 만드는 것입니다.

 

std::tr1::shared_ptr<Investment> createInvestment();

 

위와같은 방법으로 구현되면, 사용자는 무조건 해당 타입에 맞게 받아줘야합니다. 그렇지 않으면 에러가 납니다.

게다가 std::tr1::shared_ptr은 생성 시점에 자원 해제 함수(일명 삭제자)를 직접 엮을 수 있는 기능을 갖고 있기 때문에, 자원해제에 관련된 상당수의 사용자 실수를 사전 봉쇄할 수 있습니다.

 

또하나, std::tr1::shared_ptr은 '교차 DLL 문제'를 방지해준다.

 

서로 다른 DLL 사이에서 new와 delete를 했을 경우, 꼬여서 런타임 에러가 발생하는 것을 막아준다는 것입니다.

왜냐하면 기본 삭제자가 무조건 동일 DLL에서 이 짝을 수행할 수 있게 설계되어있기 때문이고, std::tr1::shared_ptr은 생성시에 그해당 DLL을 붙잡고 있기 때문입니다.

 

참고로, tr1::shared_ptr을 구현한 제품 중 가장 흔히 쓰이는 것은 Boost 라이브러리 입니다. 부스트의 shared_ptr은 일단 크기가 원시 포인터의 두배이고, 느리며, 내부 관리용 동적 메모리 까지 추가로 들지만, 사용자의 실수는 눈에 띄게 줄어들것입니다.

 

이것 만은 잊지 말자!

◆ 좋은 인터페이스는 제대로 쓰기에 쉬우며 엉터리로 쓰기에 어렵습니다. 인터페이스를 만들때는 이 특성을 지닐 수 있도록 고민하고 또 고민합시다.

◆ 인터페이스의 올바른 사용을 이끄는 방법으로는 인터페이스 사이의 일관성 잡아주기, 그리고 기본제공 타입과의 동작 호환성 유지하기가 있습니다.

◆ 사용자의 실수를 방지하는 방법으로는 새로운 타입 만들기, 타입에 대한 연산을 제한하기, 객체의 값에 대해 제약 걸기, 자원 관리 작업을 사용자 책임으로 놓지 않기가 있습니다.

◆ std::tr1::shared_ptr은 사용자 정의 삭제자를 지원합니다. 이 특징 때문에 std::tr1::shared_ptr은 교차 DLL 문제를 막아주며, 뮤텍스 등을 자동으로 잠금 해제하는데 쓸수 있습니다.


 

아래 코드를 보자

 

1
2
3
4
5
6
7
int priority(); 
 
void processWidget( std::tr1::shared_ptr<Widget> pw, int priority ); 
 
... 
 
processWidget(new Widget, priority()); //컴파일 에러

 

이 코드가 컴파일 에러가 나는 이유는 std::tr1::shared_ptr 의 생성자는 explicit 되어있기때문에 new Widget 같은 표현식이 올수 없다.

 

processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());

 

위의 코드는 컴파일에 문제가 없다.

하지만, 스마트 포인터로 받은 자원을 흘릴 가능성이 있는 함수다.

 

왜 그럴까?

C++함수는 함수를 호출할때 함수로 들어오는 인자의 평가 순서를 지니게 된다.

첫번째 인자는 (new Widget) 과 std::tr1::shared_ptr의 생성자 호출부분으로 나뉜다.

두번째 인자는 함수포인터로 함수의 호출문이 있다.

 

우선 위의 함수가 호출될때 컴파일러는 세가지 연산을 위한 코드를 만든다.

 

1. priority를 호출한다.

2. new widget 을 실행한다.

3. std::tr1::shared_ptr 생성자를 호출합니다.

 

이 연산이 실행되는 순서는 컴파일러 제작사마다 다르다.  

 

우선  std::tr1::shared_ptr은 new widget 이 실행되야지만, 실행되는 순서이다. 그렇기 때문에 순서가 변하지는 않지만, priority는 처음 호출될수도 있고, 두번째, 세번째도 호출될 수도 있습니다.

 

이때 2번과 3번 중간에 priority 함수가 호출되서 예외가 발생했다면, 자원을 막아줄줄 알고 준비한 std::tr1::shared_ptr에 저장되기도 전에 예외가 발생합니다.

 

위와같은 문제를 해결하기 위해서는?

 

 

어찌되었든 가장중요한 건!!

ProcessWidget 함수가 호출되기전에 Widget을 생성해서 스마트 포인터에 저장하는 코드를 별도의 한문장으로 하나 만들고, 그 스마트포인터를 넘기는 것입니다.

 

std::tr1::shared_ptr<Widget> pw( new Widget );

processWidget( pw, priority() );

 

이것 만은 잊지 말자!

◆ new로 생성한 객체를 스마트 포인터로 넣는 코드는 별도의 한 문장으로 만듭시다. 이것이 안되어 있으면, 예외가 발생될 때 디버깅하기 힘든 자원 누출이 초래될 수 있습니다.



메모리 할당의 순서(new)

1) 메모리 할당

2) 할당된 메모리에 대해 한 개 이상의 생성자 호출

 

메모리 해제의 순서(delete)

1) 할당된 메모리에 대해 한 개 이상의 소멸자 호출

2) 메모리 해제

 

배열을 위해 만들어지는 힙 메모리에는 대개 배열원소의 개수가 박혀 들어갑니다.

이 때문에 delete 연산자는 소멸자가 몇 번 호출 될지를 쉽게 알수 있습니다.

 

int *i = new int[5];

delete i;

 

int형의 공간 다섯개만큼의 메모리를 할당했지만,delete는 단일 객체만을 해제한다고 표시했기 때문에,

메모리가 모두 해제되지 못합니다.

 

typedef로 정의된 어떤타입의 객체를 메모리에 생성하는것은 주의를 해야한다.

왜냐하면?

 

typedef std::string AddressLines[4];  // AddressLine으로 정의하고

std::string *pal = new AddressLines; // 이렇게 생성하게 되면 나중에 명확하지 않아 혼동될수 있습니다. 배열타입을 typedef 타입으로 만들지 않는 것이 좋습니다. ( vector<string> 사용 합시다 )

 

 

즉! 

new -> delete 씁시다. 

new [] -> delete[] 씁시다.

 

이것 만은 잊지 말자!

◆ new 표현식에 [] 썼으면, 대응되는 delete 표현식에도 []를 써야합니다. 마찬가지로 new 표현식에 []를 쓰지 않았으면, 대응되는 delete 표현식에도 []를 쓰지 말야야 합니다.


+ Recent posts