36-2-나.메모리 관리

어떤 클래스를 연구할 때 가장 먼저 조사해야 하는 함수는 객체를 만드는 생성자이다. string 클래스는 모두 여섯 개의 생성자를 정의하고 있는데 원형은 다음 도표와 같다. 템플릿 함수들은 원형이 다소 복잡하므로  헤더 파일에 있는 선언문을 조금 편집하여 읽기 쉽게 정리했다. 템플릿 인수는 가급적 실제 인수로 표기하고 중간 타입들은 평이한 타입으로 바꿔서 표기하기로 하는데 예를 들어 _Elem이라고 쓰는 것보다 그냥 char라고 쓰는 것이 더 쉬울 것이다. 또한 size_type은 할당기가 정의하는 크기 타입이되 size_t(결국 unsigned)이므로 size_t로 표기하기로 한다.

 

원형

설명

string()

디폴트 생성자. 문자열을 만든다.

string(const char *s)

종료 문자열로부터 생성하는 변환 생성자

string(const string &str, int pos=0, int num=npos)

복사 생성자

string(size_t n, char c)

c n 가득 채움

string(const char *s, size_t n)

종료 문자열로부터 생성하되 n길이 확보

template<It> string(It begin, It end)

begin~end사이의 문자로 구성된 문자열 생성

 

디폴트 생성자, 복사 생성자 및 문자열 상수로부터의 생성자가 있고 문자의 반복이나 다른 문자열의 일부만을 취하는 생성자 등이 정의되어 있다. 문자열을 만들 수 있는 모든 방법에 대해 생성자가 다 정의되어 있다. 객체의 세계에서는 조금이라도 필요를 느낄만한 함수들은 다 정의되어 있다고 보면 거의 틀림없다. 제공되는 기능의 목록을 일부러 외울려고 노력할 필요도 없고 기능 자체의 상세한 사용법을 굳이 몰라도 큰 지장은 없다. OOP의 개념만 있으면 라이브러리 사용법을 습득하는 것은 아주 쉬운 일이다. 왜냐하면 내가 만들어도 당연히 저렇게 만들 것 같다는 직관력이 있기 때문이다. 다음 예제는 이 생성자들을 순서대로 호출함으로써 다양한 방법으로 string 객체를 생성한다.

 

: stringctor

#include <Turboc.h>

#include <iostream>

#include <string>

using namespace std;

 

void main()

{

     string s1("test");

     string s2(s1);

     string s3;

     string s4(32,'S');

     string s5("very nice day",8);

     char *str="abcdefghijklmnopqrstuvwxyz";

     string s6(str+5,str+10);

 

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

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

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

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

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

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

}

 

s1 ~ s6까지 여섯개의 string 객체를 생성하고 결과 확인을 위해 출력해 보았다. 실행 결과는 다음과 같다.

 

s1=test

s2=test

s3=

s4=SSSSSSSSSSSSSSSSSSSSSSSSSSSSSSSS

s5=very nic

s6=fghij

 

s1은 문자열 상수로부터 생성되는데 이때 s1의 메모리는 인수로 전달된 문자열의 길이만큼 자동으로 할당된다. "test"문자열을 저장하기 위해서는 최소 5바이트가 필요하므로 생성자에서 이 문자열을 저장할 수 있는 충분한 길이만큼 메모리를 할당할 것이다. s2는 s1을 복사하여 똑같은 내용을 가지는 객체를 생성하는데 이 생성자에서 깊은 복사를 할 것임은 쉽게 추측할 수 있다. s2는 s1으로부터 만들어지지만 생성 단계에서 같은 문자열을 가질 뿐 별개의 독립적인 객체이다. 출력 결과 s1, s2는 모두 "test"라는 문자열을 가지는데 이후 별개의 문자열을 가질 수 있다.

s3는 인수가 없는 디폴트 생성자로 생성했는데 이 경우 빈 문자열을 가지는 객체가 생성된다. 생성된 직후에는 일단 내용을 가지지 않지만 이후 대입이나 연결 등의 동작을 통해 문자열을 저장할 수 있을 것이다. 예를 들어 s3=s1+"ing"; 대입문을 실행하면 s3는 "testing"이 될 것이다. s4는 똑같은 문자를 여러 번 반복해서 얻어지는 문자열을 생성하는데 예제에서는 'S' 문자 32개로 문자열을 만들었다. 아주 단순한 생성자이지만 다음과 같은 도표를 그리고 싶을 때 이 생성자가 아주 유용하다.

'-'문자 70개로 수평선을 그었는데 문자열 상수로 이런 모양을 직접 출력하려면 사람이 일일이 세어가면서 문자열을 만들거나 루프를 돌려야 하지만 (char, int) 생성자를 사용하면 훨씬 더 쉽다. s5는 문자열 상수에서 n개의 문자만을 취해 문자열 객체를 생성한다. n이 문자열 상수보다 더 짧으면 일부 문자열만으로 문자열이 생성되며 더 길면 미리 n만큼의 메모리를 확보하되 뒤쪽의 쓰레기 문자까지도 문자열의 일부로 덧붙인다.

s6는 다른 문자열의 일정 범위로부터 문자열을 생성한다. 원형이 조금 복잡하게 선언되어 있는데 두 개의 반복자를 인수로 취해 반복자 범위안의 내용을 취한다. 반복자는 STL이 사용하는 일반화된 포인터인데 이 예제의 경우는 문자열 포인터라고 생각하면 된다. 알파벳이 저장된 str에서 5~10 범위의 문자열을 추출했으므로 s6는 "fghij"가 된다.

STL은 범위를 칭할 때 항상 시작점은 포함하지만 끝점은 포함하지 않으므로 실제 생성되는 문자열은 str의 5~9사이의 문자들이다. 여섯 번째 생성자를 사용하면 문자열이나 다른 stiring 객체의 일부 문자열로부터 새로운 문자열을 만들 수 있다. 이상의 생성자 중 앞쪽의 디폴트, 변환, 복사 생성자 셋이 자주 사용되며 나머지 생성자는 활용 빈도가 낮은 편이다.

예제 코드에는 명시적으로 보이지 않지만 객체가 파괴될 때는 파괴자가 자동으로 호출된다. string 객체는 가변적인 문자열 데이터를 객체내에 직접 가지지 않으며 동적으로 메모리를 할당하여 관리할 것임을 쉽게 추측할 수 있다. 생성자가 데이터 저장을 위해 메모리를 할당하고 있으므로 파괴자에서는 당연히 이 메모리를 해제해야 한다. 파괴자가 필요한 처리를 하므로 객체가 사라질 때 별도의 처리를 할 필요가 없으며 지역 객체일 경우 쓰다가 그냥 버리기만 하면 된다. 예제의 s1~s6 객체들은 모두 main 함수의 지역 객체이므로 별도의 정리 코드가 필요없다.

string은 객체의 생성, 파괴, 대입, 연결 등의 모든 멤버 함수와 연산자가 버퍼 길이를 자동으로 관리하도록 되어 있다. 데이터 길이만큼 버퍼를 할당하고 늘어나면 재할당하고 파괴될 때는 정리한다. 이 과정이 자동화되어 있기는 하지만 성능상의 이유로 사용자가 직접 길이를 조사하거나 제어하는 방법도 제공된다. string은 길이를 조사하고 메모리를 관리하는 몇 가지 멤버 함수를 제공한다. 다음 예제를 통해 string이 내부 메모리를 어떻게 관리하는지 구경해 보자.

 

: stringsize

#include <Turboc.h>

#include <iostream>

#include <string>

using namespace std;

 

void main()

{

     string s("C++ string");

 

     cout << s << " 문자열의 길이 = " << s.size() << endl;

     cout << s << " 문자열의 길이 = " << s.length() << endl;

     cout << s << " 문자열의 할당 크기 = " << s.capacity() << endl;

     cout << s << " 문자열의 최대 길이 = " << s.max_size() << endl;

 

     s.resize(6);

     cout << s << " 길이 = " << s.size() << ",할당 크기 = " << s.capacity() << endl;

 

     s.reserve(100);

     cout << s << " 길이 = " << s.size() << ",할당 크기 = " << s.capacity() << endl;

}

 

짧은 string 객체를 생성하고 길이와 관련된 멤버 함수들을 호출해 보았다. 실행 결과는 다음과 같다.

 

C++ string 문자열의 길이 = 10

C++ string 문자열의 길이 = 10

C++ string 문자열의 할당 크기 = 15

C++ string 문자열의 최대 길이 = 4294967294

C++ st 길이 = 6,할당 크기 = 15

C++ st 길이 = 6,할당 크기 = 111

 

size와 length는 객체에 저장된 문자열의 길이를 조사하는데 strlen 표준 함수와 기능상 동일하다. 널 종료 문자는 빼고 문자의 개수가 리턴된다. 역사적인 이유로 똑같은 함수가 두 개 제공되는데 length는 표준 이전의 길이 조사 함수이고 size는 STL이 표준이 된 후 STL과 함수명을 일관되게 맞추기 위해 새로 만들어진 것이다. 둘 중 편한대로 사용하면 된다.

capacity함수는 객체가 할당한 메모리의 양을 조사하는데 이 값은 size보다는 항상 조금 더 크다. string은 문자열이 늘어날 것에 대비하여 항상 조금의 여유분을 더 할당해 놓는데 미리 할당해 놓지 않으면 문자열이 늘어날 때마다 매번 재할당해야 하므로 속도가 느려질 것이다. 이런 미리 할당 기법은 동적 배열에서도 흔하게 사용되는 방법이다.

max_size 함수는 문자열 객체가 가질 수 있는 최대 길이를 조사하는데 32비트 시스템에서 이 값은 unsigned의 최대값보다 1 작은 값이다. 결국 string 객체의 최대 길이는 42억이나 된다는 얘기인데 어디까지나 이론적인 최대 길이일 뿐 실제로는 물리적인 메모리 한계까지만 쓸 수 있으며 이는 곧 실질적인 무한 길이를 의미한다. max_size가 리턴하는 값은 string::npos 정적 멤버 변수로 정의되어 있는데 이 값은 (unsigned)-1과 같다. 실제 객체가 이 길이를 가질 수 없으므로 npos는 검색 함수가 실패를 리턴할 때 흔히 사용된다.

string 객체는 자신이 가지는 문자열의 길이만큼 메모리를 자동으로 관리하지만 사용자가 원할 경우 길이를 강제로 변경할 수 있다. resize 함수는 문자열의 길이를 인수로 전달된 개수로 강제 조정한다. 만약 n이 현재 크기보다 더 작으면 뒤쪽 문자열은 잘라 버리며 더 크면 NULL 문자로 채우되 두 번째 인수로 채울 문자를 지정할 수 있다.  예를 들어 s1.resize(15,'*');를 호출하면 "C++ string*****"가 된다.

reserve 함수는 메모리의 여유분을 지정한 크기만큼 미리 확보한다. 조만간 메모리가 대폭 늘어날 예정이라면 string 객체가 알아서 재할당하도록 내버려 두는 것보다 미리 원하는 크기만큼 할당해 놓는 것이 유리하다. 미리 메모리를 확보해 놓으면 재할당 회수가 줄어들므로 성능상의 이점을 취할 수 있다. reserve 함수는 인수로 지정한 길이만큼 메모리를 미리 할당하되 통상 지정한 양보다 더 여유있게 할당한다. 참고로 다음 두 함수도 알아 두자.

 

void clear( );

bool empty( ) const;

 

clear 함수는 문자열을 모두 지우는데 "" 빈 문자열을 대입하는 것과 효과가 같다. empty 함수는 이 객체가 빈 문자열인지 조사하는데 "" 문자열 상수와 비교하는 것과 같으며 문자열의 길이가 0이면 true를 리턴한다.

string은 문자열에 관련된 모든 기능을 가지므로 전통적인 문자 배열 대신 사용하기에 충분하다. 그러나 때로는 string으로부터 문자 배열을 만들어야 하는 경우도 있는데 string을 인식하지 못하는 이전의 함수들을 호출할 때는 아직도 문자 배열이나 문자형 포인터가 필요하다. 예를 들어 strstr 함수를 호출하고 싶다거나 string 객체의 내용을 fwrite함수로 파일에 저장하고 싶을 때가 이에 해당한다. 다음 예제는 string객체로부터 문자열의 내용을 얻는 방법을 보여 준다.

 

: chararray

#include <Turboc.h>

#include <iostream>

#include <string>

using namespace std;

 

void main()

{

     string s("char array");

 

     cout << s.data() << endl;

     cout << s.c_str() << endl;

 

     char str[128];

     strcpy(str,s.c_str());

     printf("str = %s\n",str);

}

 

basic_string 클래스에는 문자열의 내용을 얻는 data와 c_str 두 개의 멤버 함수가 준비되어 있다. 두 함수 모두 상수 포인터를 리턴하므로 이 함수로 읽은 포인터에 대해서는 읽기만 해야 하며 값을 변경할 수는 없다. data 함수는 string 객체의 네이티브 데이터 번지를 그대로 리턴하므로 널 종료 문자가 아닐 수도 있지만 c_str은 항상 널 종료 문자열이다. basic_string템플릿의 두 번째 인수 _Traits가 지정하는 문자열 관리 방식에 따라 문자열의 형태가 결정되므로 내부적인 형태가 항상 널 종료 문자열이라고 할 수 없다.

data는 객체의 내부 데이터를 그대로 리턴하는 것이고 c_str은 널 종료 문자열이 아닌 경우 사본을 복사한 후 널 종료 문자열로 바꿔서 리턴한다는 점이 다르다. 물론 string 클래스는 널 종료 문자열이므로 string 객체에 대해서는 data와 c_str이 같겠지만 다른 basic_string 템플릿 클래스에서는 결과가 달라질 수도 있다. 그래서 C 스타일의 문자 배열로 string 객체를 복사하고 싶을 때는 c_str 멤버 함수를 사용하는 것이 옳다. 예제에서는 길이 128의 문자형 배열 str로 string 객체 s의 내용을 복사한 후 printf 함수로 출력해 보았다. string 객체의 길이 제한이 없으므로 원칙대로 하자면 size로 길이 조사 후 +1만큼 할당해서 사용해야 한다.

이상에서 알아본 바와 같이 string 클래스의 메모리 관리는 완전히 자동화되어 있다. 객체가 생성될 때 필요한만큼 메모리를 할당하고 파괴될 때 알아서 정리하며 더 긴 문자열이 대입되거나 연결되면 늘어나기도 한다. 최대 표현 길이도 충분하게 설정되어 있으므로 배열 범위를 벗어나는 것은 걱정할 필요가 없다.