항목 13 : 동적으로 할당한 배열보다는 vector와 string이 낫다

배열을 동적으로 할당할일이 있으면 그대신 vector나 string을 고려한다. 메모리 할당과 해제에 대한 부담이 없기 때문이다.

시퀀스 컨테이너의 필수사양을 모두 가지고 있기 때문에 STL에서 지원되는 모든 알고리즘 역시 그대로 적용할 수 있다.

 

한가지 여담으로 string은 대개 참조 카운트 방식으로 구현되어있는데, 멀티 스레드 환경에서의 안전성 구현으로 인한 오버헤드가 발생해서 성능이 하락할 수도 있다.

이를 회피하는 방법으로 1)참조 카운팅 기능을 끄거나 2) 참조 카운팅을 사용하지 않는 string을 구현하거나 3)vector<char>를 대신 사용한다.

vector는 참조 카운팅 방식이 아니기때문에 멀티 스레드 환경에서의 오버헤드가 발생하지 않는다.

 

 

항목 14 : reserve는 필요 없이 메모리가 재할당되는 것을 막아 준다

vector와 string은 용량이 가득 차면 할당-복사-소멸-해제의 단계를 거쳐서 재할당된다. 그런데 이 과정이 상당한 비용이 소모될수 있기 때문에 최대한 덜 이루어지는 것이 좋다.

확장되는 크기를 어느정도 예측해서 미리 넉넉하게 잡아주면 재할당 횟수가 최소화 될 수 있을것이다.

이것을 reserve 멤버 함수가 해준다.

 

reserve 멤버 함수는 당장 사용하지 않더라도 메모리를 미리 할당함으로써 재할당 횟수를 최소화 시킬 수 있다.

더불어서 재할당으로 인한 반복자, 포인터, 참조자의 무효화로 발생하는 비용도 줄일 수 있다.

 

◾ size() : 컨테이너에 담긴 요소의 개수

◾ capacity() : 컨테이너의 용량 (담길 수 있는 요소의 총 개수)

◾ resize(size_t n) : 컨테이너에 담긴 요소 개수 재설정

◾ reserve(size_t n) : 컨테이너의 용량 재설정

 

reserve를 사용해서 재할당을 피하는 방법은 컨테이너의 사용량을 정확하거나 대략적으로 알고있는 경우. 또는 필요한 최대량으로 할당한 후에 컨테이너에 모두 담기면 남은 용량을 잘라내는 두 가지 방법이 있다.

 

 

항목 15 : 잊지 말자! string은 여러 가지 방식으로 구현되어 있다는 사실을...

라이브러리별로 string의 구현 방식이 다르다. sizeof(string)의 크기 역시 다르게 나온다.

 

 

항목 16 : 기존의 C API에 vector와 string을 넘기는 방법을 알아두자

vector는 연속 메모리 컨테이너이기 때문에 C API의 인자로 넘겨주는것이 그리 어렵지 않다.

 

void doSomething(const int* pInts, size_t numInts);

if (!v.empty()) doSomething(&v[0], v.size());

단, empty일때를 주의해야한다. 존재하지도 않는 요소의 주소를 넘겨줄수는 없다.

 

void doSomething(const char* pString);

doSomething(s.c_str());

string은 조금 다르다. empty여도 NULL을 반환하기 때문에 사용에 문제가 없다.

 

vector나 string이나 공통적으로 넘겨지는 매개변수는 const에 대한 포인터이다. 읽기만 가능할 뿐 수정이 불가능하다.

vector는 그나마 컨테이너 요소의 주소를 넘기기 때문에 const로 넘기지 않아도 요소의 값 변경이 가능하긴 하지만, string은 c_str로 반환된 포인터가 컨테이너에 존재하는 요소의 포인터라는 보장이 없기 때문에 오직 읽기만 가능하다.

 

C API를 통해 STL 객체를 초기화 하고 싶은 경우에는 vector를 활용하면 된다. vector만 유일하게 C/C++ 배열과 동일한 메모리 배열 구조를 가졌기 때문이다.

 

/* vector */
size_t fillArray(double* pArray, size_t arraySize);

std::vector<double> vd(maxNumDoubles);
vd.resize(fillArray(&vd[0], vd.size());


/* string */
size_t fillString(char* pArray, size_t arraySize);

std::vector<char> vc(maxNumChars);
size_t charsWritten = fillString(&vc[0], vc.size());
std::string s(vc.begin(), vc.begin() + charsWritten);

 

 

항목 17 : 쓸데없이 남은 용량은 "바꿔치기(swap) 묘수"를 써서 없애 버리자

예를 들어 어떤 오디션 프로그램의 참가 신청자의 목록을 vector로 관리한다고 했을 때, 처음에는 수많은 신청자 덕분에 용량을 매우 크게 할당하겠지만 프로그램이 진행됨에 따라 인원이 점점 줄어들게 될 것이다.

하지만 실제로 줄어든것은 size일뿐 capacity가 아니다. 더 이상 사용되지도 않는 메모리를 계속 가지고 있는것은 낭비이기 때문에 지속적으로 크기를 줄일 필요가 있다.

 

std::vector<Contestant>(contestants).swap(contestants);

Contestant 타입은 참가자, contestants는 실제 참가자 목록이고 해당 목록을 복사하여 임시 객체를 만들어서 실제 참가자 목록과 swap하면 딱 맞는 메모리의 재할당이 이루어진다.

임시 객체는 rvalue이기 때문에 해당 문장의 실행이 끝나면 메모리에서 소멸된다.

 

std::string s;
...
std::string(s).swap(s);

string에도 사용할 수 있다.

 

std::vector<Contestant> v;
std::string s;
...
std::vector<Contestant>().swap(v); // v 초기화 및 용량 최소화
std::string().swap(s); // s 초기화 및 용량 최소화

기본 생성자로 생성된 임시 객체와 바꿔치기 함으로써 구현 코드가 허용하는 최소치로 줄일수도 있다.

 

바꿔치기 될 때, 반복자, 포인터, 참조자도 모두 바뀌는것을 주의해야한다.

 

std::vector<int> v;
auto i = v.begin();
...
std::vector<int>().swap(v);
/* i는 무효화 된다 */

 

 

항목 18 : vector<bool> 보기를 돌같이 하자

vector<bool>은 STL 컨테이너처럼 보이지만 실제로는 아니다. 템플릿 특수화로 만들어진 특별한 타입이다.

 

T* p = &c[0];

c가 타입 T의 객체를 담는 컨테이너라면 위의 코드는 당연히 성립되어야 한다.

 

bool b[10]; // 요소당 1바이트
std::vector<bool> vb; // 요소당 1비트
...
bool* pb = &v[0]; // error

그런데 bool은 성립되지 않는다. bool이 1바이트를 사용하는것과 달리 vector<bool>은 1비트를 사용하기 때문에 데이터 표현 방식이 맞지 않기 때문이다.

vector<T>:operator[]의 반환 타입은 T&여야 하는데 비트에 대한 참조자는 반환할 수 없다.

 

만약 진짜 bool을 저장하고 싶다면 deque를 사용해야 한다.

'도서 > Effective STL' 카테고리의 다른 글

반복자(Iterators)  (0) 2022.12.20
STL 연관 컨테이너 (2)  (0) 2022.12.20
STL 연관 컨테이너 (1)  (0) 2022.12.20
효과적인 컨테이너 요리법 (2)  (0) 2022.12.16
효과적인 컨테이너 요리법 (1)  (0) 2022.12.16

+ Recent posts