책정리/혼자 연구하는 C,C++ 2

36장 표준 라이브러리

GONII 2015. 3. 11. 18:00

36-1 iostream

가. 입출력 스트림의 구조

C++의 표준 입출력 스트림은 여러 가지 복잡한 상황에 대해서도 입출력을 처리할 수 있도록 확장 가능한 클래스 계층을 구성하고 있다. C++의 헤더 파일을 기준으로 그려본 간단한 클래스 계층도이다.

ios_base 클래스는 입출력과 관련된 여러 가지 상수나 플래그들을 가지며 이 클래스로부터 입출력 클래스들이 파생된다. 출력 클래스인 basic_ostream의 선언문은 다음과 같다

template<class _Elem, class _Traits>

class basic_ostream : virtual public basic_ios<_Elem, _Traits>

{

...

   

:: _Elem

출력하는 데이터의 기본 타입이다. 통산 문자열 형태로 출력되므로 가거에는 char 타입의 문자들을 출력했으나 모든 문자를 16비트 코드를 표현하는 유니코드 환경에서는 wchar_t가 될 수도 있다.

   

:: _Traits

이 인수는 출력 문자열의 형태와 관리 방법을 정의하는 객체이다. 보편적으로 널 종료 문자열을 많이 사용하는데 시작 번지로부터 문자가 나타나며 끝은 NULL 문자로 표현하는 방식이다. C/C++을 주로 사용하는 사람들에게는 이 문자열 형태가 아주 익숙하겠지만 이는 문자열을 표현하는 여러 가지 방법 중의 하나일 뿐이다.

   

basic_ostream 클래스는 인수로 주어진 타입을 요소로 가지는 문자열을 표현하되 문자열을 다루는 방식은 _Traits 객체에 따라 달라진다. 이 클래스로부터 두 개의 특수화된 클래스가 정의된다.

typedef basic_ostream<char, char_traits<char> > ostream;

typedef basic_ostream<wchar_t, char_traits<wchar_t> > wostream;

ostream은 char 타입의 문자열을 출력하는 클래스이며 char_traits<char> 객체는 C에서 보편적으로 사용되는 널 종료 문자열을 관리하는 객체이다. wostream은 wchar_t 타입의 문자열, 즉 유니코드문자열을 출력하는 클래스이다. 표준 라이브러리는 8개의 입출력 객체를 미리 정의하는데 iostream 헤더 파일을 보면 다음과 같은 선어문을 볼 수 있다.

extern istream cin;

extern ostream cout;

extern ostream cerr;

extern ostream clog;

extern wistream wcin;

extern wostream wcout;

extern wostream wcerr;

extern wostream wclog;

cin이 표준 입력 객체이며 cout이 표준 출력 객체이다. cerr는 표준 에러 객체이되 버퍼를 사용하지 않고 곧바로 출력을 내보낸다는 점이 cout과 다르며 clog는 디버깅을 위한 기록 객체이며 버펄르 사용한다.

표준 입출력 객체들은 효율적인 입출력 관리를 위해 내부적으로 버퍼를 사용한다. 입출력할 때마다 한 문자씩 장치로 직접 출력하면 느리기 때문에 버퍼가 필요하다. 이 버퍼는 streambuf라는 내부 클래스에 의해 자동으로 관리된다.

나. 출력 스트림

표준 출력 객체는 cout이며 라이브러리에 의해 이름이 이미 정해져 있다. cout으로 출력을 보낼 때는 << 연산자(Insertion)를 사용하는데 C++의 모든 기본 타입과 문자형 포인터에 대해 << 연산자가 오버로딩되어 있으므로 타입에 상관없이 보내기만 하면 << 연산자가 타입을 해석하여 적절하게 출력한다. << 연산자가 출력 객체에 대한 레퍼런스를 리턴하므로 연쇄적인 출력도 가능하다.

<<연산자는 원래 비트를 왼쪽으로 이동시키는 쉬프트 연산자인데 ostream 클래스가 이 연산자를 오버로딩하여 삽입 연산자로 사용하고 있다.

  • 예제 couterror

#include <iostream>

using namespace std;

   

void main()

{

bool bMan = true;

cout << "당신은 " << bMan==true ? "남자" : "여자" << "입니다." << endl;

}

컴파일해 보면 cout에 == 연산자가 정의되어 있지 않다는 에러로 처리되는데 삼항 조건 연산자보다 <<연산자가 우선 순위가 더 높기 때문이다. bMan을 cout으로 먼저 출력하고 리턴된 cout을 true와 상등 비교했으므로 틀린 연산이 된 것이다. 괄호를 싸서 먼저 연산하도록 하면 결과가 나온다.

cout << "당신은 " << (bMan==true ? "남자" : "여자") << "입니다." << endl;

cout은 인수로 전달된 출력 대상을 내부적인 변환 규칙에 따라 문자열로 변환하여 화면으로 출려한다. 정수123은 문자열"123"으로 실수 4.5678은 "4.5678"로 변환할 것이다. 만약 이런 내부적인 변환 규칙에 변활르 주고 싶다면 조정자(mainpulator)를 사용한다.

  • 예제 coutradix

#include <iostream>

using namespace std;

   

void main()

{

int i = 1234;

hex(cout);

cout << i << endl;

   

cout << "8진수 : " << oct << i << endl;

cout << "16진수 : " << hex << i << endl;

cout << "10진수 : " << dec << i << endl;

}

  • 예제 coutwidth

#include <iostream>

using namespace std;

   

void main()

{

int i = 1234;

int j = -567;

   

// 출력폭 지정

cout << i << endl;

cout.width(10);

cout << i << endl;

cout.width(2);

cout << i << endl;

   

// 채움 문자 지정

cout.width(10);

cout.fill('_');

cout << i << endl;

cout.fill(' ');

   

// 정렬 지정

cout.width(20);

cout << left << j << endl;

cout.width(20);

cout << right << j << endl;

cout.width(20);

cout << internal << j << endl;

}

출력폭은 width멤버 함수로 지정하는데 printf의 %10d랑 동일한 기능이다. internal은 부호나 진법 지정(0x 등)은 왼쪽에, 숫자는 오른쪽에 출력하는 정렬 방식이다.

  • 예제 coutfloat

#include <iostream>

using namespace std;

   

void main()

{

double d = 1.234;

   

// 실수의 정밀도 지정

cout << d << endl;

cout.precision(3);

cout << d << endl;

cout.precision(10);

cout << showpoint << d << endl;

cout.precision(6);

   

// 실수 출력방식

cout << fixed << d << endl;

cout << scientific << d << endl;

}

지정한 자리수보다 신수가 짧을 경우는 후행 제로를 출력하지 않는데 후행 제로까지 정확하게 출력하고 싶다면 showpoint라는 조정자를 사용한다.

  • 예제 coutmanip

#include <iostream>

using namespace std;

   

void main()

{

int i = 1234;

double d = 56.789;

char *str = "string";

bool b = true;

   

// bool형 출력 방식

cout << b << endl;

cout << boolalpha << b << endl;

   

// 진법 접두 출력 및 대소문자

cout << hex << i << endl;

cout << showbase << i << endl;

cout << uppercase << i << endl;

   

// + 양수 기호 표시

cout << dec << showpos << i << endl;

}

다. 입력 스트림

C++의 표준 입력 스트림은 cin 객체이다. cin 객체로 입력을 받아 >> 연산자(Extraction)로 대상 변수로 보내기만 하면 된다. >> 연산자가 대부분의 기본 타입에 대해 오버로딩되어 있기 때문에 타입에 상관없이 입력을 받을 수 있다. cout과 마찬가지로 >> 연산자가 cin 객체의 레퍼런스를 리턴하므로 연쇄적으로 입력을 받을 수도 있다.

  • 예제 cin

#include <iostream>

using namespace std;

   

void main()

{

int i ;

char str[128];

   

cin >> i;

cout << i << endl;

cin >> str;

cout << str << endl;

}

cin 입력 객체와 >> 연산자는 다음과 같은 중요한 네 가지 특징이 있다

  1. 공백은 건너뛰고 입력받는다. 공백은 실제 입력받고자 하는 대상이 아니며 데이터의 구분을 위한 구분자로 삽입되는 경우가 많기 때문에 입력값의 일부라고 볼 수 없다. 그래서 앞쪽의 공백은 일단 건너뛰고 유효한 문자부터 읽기 시작한다. 여기서 공백이란 스페이스뿐만 아니라 탭과 개행 코드도 포함된다.
  2. 읽을 수 있는 유효한 입력까지만 읽으며 무효한 문자를 만나는 즉시 읽기를 중지한다. 예를 들어 정수값을 읽는다면 정수를 구성하는 아라비아 숫자와 +,-부호 기호 등만 유효한 입력으로 간주하며 숫자가 아닌 입력이 들어오면 읽기를 중지한다.
  3. 앞 규칙에 의해 읽지 못한 데이터는 버퍼에 그대로 남겨지며 다음번 읽을 때 버퍼에 남아 있는 값이 읽혀진다. 만약 버퍼에 저장된 값이 쓸모없는 값이라면 버퍼를 비운 후 다시 입력을 받아야 한다.
  4. 문자열의 경우 문자열 배열에 저장되는데 C에서와 마찬가지로 배열의 끝 점검은 하지 못한다. 그래서 입력받을 데이터를 저장할만한 충분한 공간을 제공해야 한다.

   

  • 예제 cstring

#include <iostream>

using namespace std;

   

void main()

{

char ch;

char str[128];

   

cin >> ch;

cout << ch << endl;

cin >> str;

cout << str << endl;

}

>>연산자가 공백을 무조건 건너뛰어 버리기 때문에 ch, str은 공백을 입력받지 못한다. 그래서 cin 객체는 이런 문제를 해결하기 위해 >> 연산자와는 별도로 단일 문자 입력과 문장 입력 함수를 따로 제공한다.

int get(void);

basic_istream& get(char& c);

basic_istream& getline(char *s, streamsize n, char delim='\n');

첫 번째 get함수는 C의 getch 함수와 유사해서 사용하기 편리하고, 두번째 get함순느 입력 객체의 레퍼런스를 리턴하므로 연쇄적인 입력이 가능하다는 장점이 있다.

  • 예제 cstring2

#include <iostream>

using namespace std;

   

void main()

{

char ch;

char str[128];

   

cin.get(ch);

cout << ch << endl;

cin.get(ch);

   

cin.getline(str, 128);

cout << str << endl;

}

  • 예제 cinerror

#include <iostream>

using namespace std;

   

void main()

{

int i;

   

if( cin >> i )

{

cout << i << endl;

}

else

{

cout << "실패" << endl;

}

}

>>연산자는 입력 객체의 레퍼런스를 리턴하는데 실패할 경우 입력 객체의 에러 비트를 설정하는 방식으로 연산식 전체의 평가 결과를 false로 만드므로 입력문 자체를 if문 안에 넣어 에러 발생 여부를 조사할 수 있다.

cin 객체는 내부적으로 에러 발생 여부를 표시하는 세 개의 플래그를 유지하는데 입력 동작 후 이 플래그들의 값을 점검하여 어떤 에러가 발생했는지 알 수 있다.

플래그

설명

failbit

입력에 실패했다는 뜻이다. 정수를 입력받는데 문자가 입력된 경우 1이 된다.

eofbit

파일 끝이라는 뜻이다. 더 이상 읽을 문자가 없으므로 에러를 리턴한다.

badbit

스트림이 물리적으로 손상되어 읽을 수 없다.

goodbit

상기 세 에러가 발생하지 않았다는 뜻이며 0으로 정의되어 있다.

각각의 에러가 발생했는지 조사하는 fail(), bad(), eof(), good() 멤버 함수가 정의되어 있으므로 이 함수들을 호출하여 입력 객체의 상태가 어떠한지를 조사할 수 있다.

  • 예제 cinerrorbit

#include <iostream>

using namespace std;

   

void main()

{

int i;

   

cin >> i ;

if( cin.good() )

{

cout << i << endl;

}

else

{

cout << "실패" << endl;

}

}

cin 객체는 일단 에러가 발생하면 계속 에러 상태를 유지하며 이 상태에서는 어떠한 입력도 받지 못한다. 그래서 별도의 함수로 에러 상태를 리셋해야 하는데 이 때는 다으 멤버 함수들을 사용한다.

iostate rdstate() const;

void setstate(iostate state);

void clear(iostate state = goodbit);

rdstate는 에러 플래그의 값을 리턴하며 setstate는 에러 플래그를 설정하거나 해제한다. clear도 에러 플래그를 변경하는데 지정한 플래그값만 남기고 나머지를 모두 리셋한다는 점이 setstate와는 다르다.

cin.setstate(failbit);        // failbit를 설정한다. 나머지 비트는 원래 상태를 유지

cin.clear(failbit);                // failbit를 설정하고 나머지 비트를 리셋

멤버 함수

설명

ignore

지정한 길이만큼 또는 지정한 문자가 나올 때까지 데이터를 무시한다. 버퍼에 들어 있는 데이터를 읽어서 버리고자 할 때 이 함수를 쓴다.

peek

버퍼에 있는 데이터를 읽기만 하고 제거하지는 않는다. 어떤 데이터가 버퍼에 있는지 살짝 들여다보기만 할 때 이함수를 사용한다.

gcount

앞 입력문에 의해 실제로 읽혀진 데이터의 길이를 조사한다. >> 연산자로 읽은 길이는 조사할 수 없으며 get, getline, read등의 함수로 읽은 길이만 조사할 수 있다.

putback

특정 데이터를 버퍼에 다시 밀어 넣는다. 마치 어떤 문자가 입력된 것처럼 만들고 싶을 때 이 함술르 사용하는데 이 함수가 밀어 넣은 데이터는 다음 번 입력 함수에 의해 꺼내질 것이다.

라. 파일 입출력

입출력 스트림으로 파일 입출력도 할 수 있는데 C에서와 마찬가지로 파일이나 콘솔이나 어차피 스트림이라는 면에서는 동일하므로 똑같은 방법으로 다룰 수 있다.

앞쪽 입출력 스트림의 클래스 계층도를 보면 파일 입출력 스트림인 basic_i(o)stream은 콘솔용 입출력 클래스로부터 상속된다. 따라서 >>, << 연산자 및 멤버 함수, 조정자 등을 그대로 사용할 수 있다. 다만 입출력 대상이 파일이라는 메모리 외부의 장치이므로 빠른 애겟스를 위해 열고 닫는 동작과 섬세한 에러 처리를 해야 한다는 정도만 다르다. 일반화를 위해 파일 입출력 클래스도 템플릿으로 정의되어 있다.

typedef basic_ifstream<char, char_traits<char> > ifstream;

typedef basic_ofstream<char, char_traits<char> > ofstream;

typedef baic_ifstream<wchar_t, char_traits<wchar_t> > wifstream;

typedef basic_ofstream<wchar_t, char_traits<wchar_t> > wofstream;

  • 예제 cppfilewrite

#include <iostream>

#include <fstream>

using namespace std;

   

void main()

{

ofstream f;

   

f.open("c:\\cpptest.txt");

f << "string" << 1234 << endl;

f.close();

}

객체를 선언하고 오픈할 파일의 이름을 곧바로 전달할 수도 있다.

ofstream f("c:\\cpptest.txt");

출력할 때는 << 연산자를 사용하는데 콘솔에서와 마찬가지로 모든 기본 타입에 대해 출력할 수 있으며 연쇄적인 출력도 가능하다. 출력을 완료했으면 close 함수로 파일을 닫는다.

  • 예제 cppfileread

#include <iostream>

#include <fstream>

using namespace std;

   

void main()

{

ifstream f;

char str[128];

int i;

   

   

f.open("c:\\cpptest.txt");

f >> str >> i ;

cout << str << i << endl;

f.close();

}

파일은 메모리 외부에 존재하기 때문에 에러가 발생할 확률이 아주 높으므로 오픈할 때 파일이 제대로 열렸는지 항상 확인해야 한다. 에러 발생 여부는 오픈 직후의 객체의 is_open 멤버 함수로 확인할 수 있는데 이 함순느 인수를 취하지 않으며 성공 여부를 표현하는 bool값을 리턴한다.

  • 예제 is_open

#include <iostream>

#include <fstream>

using namespace std;

   

void main()

{

ifstream f;

char str[128];

int i;

   

f.open("c:\\neverexist.txt");

if( f.is_open() )

{

cout << "파일 열기 성공" << endl;

f.close();

}

else

{

cout << "파일 열기 실패" << endl;

}

}

파일을 여는 open 함수의 원형은 다음과 같다. 대상 파일의 경로 외에도 파일을 어떻게 열 것인지 파일 모드를 지정하는 두 번째 인수가 있으며 디폴트 값이 지정되어 있다. 출력용과 입력용의 디폴트 모드가 다르게 설정되어 있는데 입력용은 읽을 수만 있고 출력용은 쓸수만 있다.

void ifostream::open(const char *s, ios_base::openmode mode = ios_base::in);

void ofstream::open(const char *s, ios_base::openmode mode = ios_base::out | ios_base::trunc);

파일 모드는 ios_base에 정의된 상수들이며 여러 개의 모드를 OR 연산자로 묶어서 지정하 ㄹ수 있다.

모드

설명

ios_base::out

출력용으로 파일을 연다

ios_base::in

입력용으로 파일을 연다

ios_base::app

파일 끝에 데이터를 덧붙인다. 데이터를 추가하는 것만 가능하다.

ios_base::ate

파일을 열자마자 파일 끝으로 FP를 보낸다. FP를 임의 위치로 옮길 수 있다.

ios_base::trunc

파일이 이미 존재할 경우 크기를 0으로 만든다

ios_base::binary

이진 파일 모드로 연다

  • 예제 cppfilecopy

#include <iostream>

#include <fstream>

using namespace std;

   

void main()

{

ifstream src("c:\\dummy.txt", ios_base::in | ios_base::binary);

if( !src.is_open() )

{

cout << "원본 파일이 없습니다." << endl;

}

ofstream dest("c:\\dummy2.txt", ios_base::out | ios_base::trunc | ios_base::binary );

char buf[100000];

int nread;

   

for( ;; )

{

src.read(buf, 10000);

nread = src.gcount();

if( nread == 0 ) break;

dest.write(buf, nread);

}

src.close();

dest.close();

}

파일 복사는 파일에 있는 내용을 그대로 읽어서 사본을 만드는 것이므로 읽고 쓰는 중에 어떠한 변환도 할 필요가 없으며 그래서 binary 플래그를 지정하여 이진 모드로 열었다. 이진 모드의 파일을 읽고 쓸 땐느 다음 함수들을 사용한다.

basic_istream& read(char *s, streamsize n);

basic_ostream& write(const char *s, streamsize n);

읽고 쓸 데이터의 시작 번지와 크기를 인수로 전달한다. 복사하는 방법은 아주 원론적인데 원본에서 읽어서 대상 파일로 출력하기를 원본을 다 읽을 때까지 반복하면 된다. 예제에서는 10K 크기의 버퍼를 준비하고 10K 단위로 원본을 읽어 대상 파일을 보내는데 단, 파일 끝에서는 실제 읽은 바이트만큼만 출력해야 한다. read 함수가 실제 읽는 길이는 gcount 함수로 조사할 수 있다.

파일 액세스 함수들은 항상 파일의 현재 위치(FP)를 참조하며 읽고 쓴 후에 FP를 뒤쪽으로 옮기므로 순차적으로 파일을 액세스 할 수 있다. FP를 임의의 위치로 옮길 때는 다음 두 함수를 사용하는데 입력용, 출력용의 FP를 따로 유지하므로 함수가 두 개로 나뉜다.

basic_istream& seekg(oof_type off, ios_base::seek_dir way);

baic_ostream& seekp(off_type off, ios_base::seek_dir way);

첫 번째 인수 off는 어디로 이동할 것인지 거리를 지정하며 두 번째 인수는 이동의 기준점인데 ios_base::beg 파일의 선두를 기준으로 하며 ios_base::cur는 현재 위치, ios_base::end는 파일의 끝을 기준으로 한다. FP를 지정하는 방식은 C의 fseek 함수와 사실상 동일하다. FP의 현재 위치를 조사하는 함수는 tellp, tellg인데 역시 두 개의 FP에 대해 함수가 각각 제공된다.

36-2 string

가. 문자열 클래스

C/C++은 언어 차원에서 문자열 타입을 제공하지 않기 때문에 다소 불편한 면이 있다. 대신 문자형 배열로 문자열을 표현하는데 여러 모로 귀찮은 일들이 많고 때로는 미리 정한 배열 크기를 벗어나면 위험해지기도 한다.

string클래스는 string헤더 파일에 정의되어 있으며 std 네임스페이스에 포함되어 있다.그래서 이 클래스를 쓰고 싶다면 string헤더를 인클루드하고 std 네임스페이스에 대해 using 지시자를 사용해야 한다. string은 템플릿 기반의 클래스이므로 핵심 코드들은 거의 대부분 헤더 파일에 작성되어 있으며 이 헤더를 열어 보면 소스를 직접 볼 수 있다.

template<class _Elem, class _Traits = char_traits<_Elem>, class _Ax = allocator<_Elem> >

class basic_string { 멤버 목록 };

뒤쪽 두 개의 인수에 대해서는 디폴트가 적용되는데 대부분의 경우 디폴트가 사용되지만 원하면 변경할 수 있다. 앞쪽 두 인수의 의미는 basic_ostream의 경우와 같으며, _Ax인수는 문자열 관리를 위한 메모리를 어떻게 할당하고 해제할 것인가를 지정하는 할당기이다. 문자열 클래스는 가변 길이를 다룰 수 있어야 하므로 메모리를 동적 할당 해야 한다. 디폴트인 allocator<_Elem> 은 C++의 할당 연산자인 new, delete를 사용하는데 원한다면 다른 것으로 바꿀 수 있다.

basic_string템플릿은 충분한 확장성과 일반성을 고려하여 작성되현재 문자를 표현하기 위해 사용할 수 있는 타입은 char, wchar_t 두 가지가 있는데 이 두가지에 대해서는 다음과 같은 특수화 버전이 미리 선언되어 있다.

typedef basic_string<char> string;

typedef basic_string<wchar_t> wstring;

string은 ANSI문자열이며 wstring은 유니코드 문자열을 표현한다. 두 클래스 모두 첫 번째 인수만 지정했으므로 디폴트에 의해 널 종료 문자열이며 new, delete로 메모리를 관리한다.

string클래스는 그 자체로 독립적이기는 하지만 STL과도 깊은 연관이 있다. 반복자를 사용할 수 있으며, reverse, sort 등의 STL 알고리즘을 string에도 그대로 적용할 수 있고 컨테이너와 함께 사용할 수도 있다.

나. 메모리 관리

어떤 클래스를 연구할 때 가장 먼저 조사해야 하는 함수는 객체를 만드는 생성자이다. string클래스는 모두 여섯 개의 생성자를 정의하고 있는데 원형은 다음 도표와 같다.

원형

설명

string()

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

string(const char* s)

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

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

복사 생성자

string(size_t n, char c)

c를 n개 가득 채움

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

begin_end사이의 문자로 구성된 문자열 생성

  • 예제 stringctor

#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;

}

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

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

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

  • 예제 stringsize

#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;

}

size와 length는 strlen 표준함수와 기능상 동일하다. 널 종료 문자는 빼고 문자의 개수가 리턴된다.

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

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

string 객체는 자신이 가지는 문자열의 길이만큼 메모리를 자동으로 관리하지만 사용자가 원할 경우 길이를 강제로 변경할 수 있다. resize 함수는 문자열의 길이를 인수로 전달된 개수로 강제 조정한다. n이 현재 크기보다 작으면 뒤쪽 문자열은 잘라 버리며 더 크면 NULL 문자로 채우되 두 번째 인수로 채울 문자를 지정하 ㄹ수 있다.

reserve 함수는 메모리의 여유분을 지정한 크기만큼 미리 확보한다. reserve 함수는 인수로 지정한 길이만큼 메모리를 미리 할당하되 통상 지정한 양보다 더 여유있게 할당한다.

clear 함수는 문자열을 모두 지우는데 ""빈 문자열을 대입하는 효과와 같다.

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

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

  • 예제 chararray

#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);

}

data는 객체의 내부 데이터를 그대로 리턴하는 것이도 c_str은 널 종료 문자열이 아닌 경우 사본을 복사한 후 널 종료 문자열로 바꿔서 리턴한다는 점이 다르다. data와 c_str이 같겠지만 다른 basic_string 템플릿 클래스에서는 결과가 달라질 수도 있다. 그래서 C 스타일의 문자 배열로 string 객체를 복사하고 싶을 때는 c_str멤버 함수를 사용하는 것이 옳다.

string 객체의 길이 제한이 없으므로 원칙대로 하자면 size로 길이 조사 후 +1만큼 할당해서 사용해야 한다.

다. 입출력

string 객체를 화면으로 출력할 때는 << 연산자를 이용하여 cout으로 보내기만 하면 된다. 이 출력코드가 동작하는 이유는 cout과 string 객체를 인수로 취하는 << 전역 연산자가 오버로딩되어 있기 때문이다. cout이 string을 알아서 인식하는 것이 아니라 string 헤더 파일에 다음과 같은 연산자가 정의되어 있으므로 이런 출력 코드가 동작한다.

template<class _Elem, class _Traits, calss _Alloc>

inline basic_stream<_Elem, _Traits>& __cdcel operator<<(basic_ostream<_Elem, _Traits>& _Ostr, const basic_string<_Elem, _Traits, _Alloc>& _Str);

이 선언문을 좀 읽기 쉽게 정리해보면 다음과 같다

ostream& operator<<(ostream& cout, string &s);

cin 표준 입력을 통해 문자열을 받는 받는 것도 string 헤더 파일이 <<연산자를 정의하는 것과 마찬가지로 >>연산자도 정의하고 있기 때문이다. >>연산자는 한 단어만 입력할 수 있는데 getline 전역 함수를 사용하면 개해 ㅇ코드 전까지의 한 행을 모두 입력받을 수 있다.

  • 예제 stringin

#include <iostream>

#include <string>

using namespace std;

   

void main()

{

string name, addr;

   

cout << "이름을 입력하세요" << endl;

cin >> name;

cout << "입력한 이름은 " << name << "입니다." << endl;

cin.ignore();

cout << "주소를 입력하세요" << endl;

getline(cin, addr);

cout << "입력한 주소는 " << addr << "입니다." << endl;

}

>>연산자는 공백을 만나면 입력을 완료해 버리므로 주소 같은 긴 문장을 입력받지는 못한다. 또한 cin은 문자열 입력 후 버퍼에 개행 코드를 남겨 두므로 ignore 함수로 이 개행 코드를 버리는 처리도 필요하다.

getline으로 문장을 입력받을 때 이 함수가 입력받은 문자열의 길이만큼 string 객체의 길이를 자동으로 관리하므로 아무리 긴 문자열이 입력되더라도 배열 범위를 넘어서는 것은 걱정하지 않아도 된다.

문자열의 개별 문자들을 액세스하고 싶을 때는 [ ]연산자 또는 at멤버 함수를 사용한다.

char& operator[](size_type _Off)

char& at(size_type _Off);

const char& operator[](size_type _Off) const

const char& at(size_type _Off) const;

  • 예제 stringat

#include <iostream>

#include <string>

using namespace std;

   

void main()

{

string s("korea");

size_t len, i;

   

len = s.size();

for( i = 0 ; i < len ; i++ )

{

cout << s[i];

}

cout << endl;

s[0] = 'c';

   

for( i = 0 ; i < len ; i++ )

{

cout << s.at(i);

}

cout << endl;

}

라. 대입 및 연결

string객체에 다른 문자열이나 문자를 대입할 때는 = 연산자를 사용한다.

string& operator=(char ch);

string& operator=(const char* str);

string& operator(const string& other);

문자열을 연결할 땐느 += 연산자를 사용한다.

string& operator+=(char ch);

string& operator+=(const char* str);

string& operator+=(const string& other);

  • 예제 stringequalplus

#include <iostream>

#include <string>

using namespace std;

   

void main()

{

string s1("야호 신난다.");

string s2;

s2 = "임의의 문자열";

cout << s2 << endl;

s2 = s1;

cout << s2 << endl;

s2 = 'a';

cout << s2 << endl;

   

s1 += "문자열 연결";

cout << s1 << endl;

s1 += s2;

s1 += '!';

cout << s1 << endl;

   

string s3;

s3 = "s1:"+s1+"s2:"+s2+'.';

cout << s3 << endl;

}

대입, 연결 연산자는 모두 string형의 레퍼런스를 리턴하므로 연쇄적인 대입이나 연결도 가능하다. +연산자도 string의 멤버는 아니지만 프렌드로 정의되어 있어 문자열, string 객체, 단일 문자를 연쇄적으로 연결할 수 있다.

다른 문자열의 일부만 대입하거나 연결하고 싶을 땐느 연산자를 쓸 수 없으며 다음 두 함수를 사용해야 한다.

string& assign(const string& _str, size_t off, sizt_t count);

string& append(const string& _str, size_t off, size_t count);

  • 예제 AssignAppend

#include <iostream>

#include <string>

using namespace std;

   

void main()

{

string s1("1234567890");

string s2("abcdefghijklmnopqrstuvwxyz");

string s3;

   

s3.assign(s1, 3, 4);

cout << s3 << endl;

s3.append(s2, 10, 7);

cout << s3 << endl;

}

다음 두 함수는 문자 배열에 문자열을 복사하거나 string 객체끼리 교환한다.

size_type copy(value_type* _ptr, size_type _count, size_type _off = 0 ) const;

void swap(basic_string& _str);

  • 예제 CopySwap

#include <iostream>

#include <string>

using namespace std;

   

void main()

{

string s("1234567890");

char str[128] = "abcdefghijklmnopqrstuvwxyz";

   

s.copy(str, 5, 3);

cout << str << endl;

   

string s1("dog");

string s2("cow");

cout << "s1:" << s1 << " s2:" << s2 << endl;

s1.swap(s2);

cout << "s1:" << s1 << " s2:" << s2 << endl;

}

str의 4번째 위치에 문자열을 복사하고 싶다면 s1.copy(str+4, 5, 3)으로 포인터 연산을 하여 호출할 수 있다.

마. 삽입과 삭제

문자열 중간에 다른 문자 또는 문자열을 삽입하는 insert함수가 있다.

string& insert(size_t pos, const char* ptr);

string& insert(size_t pos, const char* ptr, size_t count);

string& insert(size_t pos, const string& str);

string& insert(size_t pos, const string& str, int off, int count);

void push_back(char ch);

삽입 동작은 문자열에 내용을 추가하는 append와 유사하되 삽입하는 지점을 인수로 전달한다는 것만 다를 뿐이다. push_back 함수는 문자 하나만 제일 끝에 추가하는 함수인데 STL 형식의 함수를 string 객체에 정의해 놓은 것이다.

다음은 문자열의 일부를 삭제하는 함수이다.

string& erase(size_t pos = 0, size_t count = npos);

  • 예제 stringinsert

#include <iostream>

#include <string>

using namespace std;

   

void main()

{

string s1("1234567890");

string s2("^_^");

   

cout << s1 << endl;

s1.insert(5, "XXX");

cout << s1 << endl;

s1.insert(5, s2);

cout << s1 << endl;

s1.erase(5, 6);

cout << s1 << endl;

}

다음은 문자열의 일부를 다른 내용으로 바꾸는 대체 함수이다.

string& replace(size_t pos, size_t num, const char *ptr);

  • 예제 stringreplace

#include <iostream>

#include <string>

using namespace std;

   

void main()

{

string s1 = "독도는 한국땅";

   

cout << s1 << endl;

s1.replace(7, 4, "대한민국");

cout << s1 << endl;

}

다음은 string 객체의 일부 문자열을 추출하여 새로운 string 객체를 생성한다.

string substr(size_t off = 0, size_t count = npos) const;

  • 예제 substr

#include <iostream>

#include <string>

using namespace std;

   

void main()

{

string s1 = "123456789";

string s2 = s1.substr(3,4);

   

cout << "s1:" << s1 << endl;

cout << "s2:" << s2 << endl;

}

바. 비교와 검색

관계 연산자 외에 compare라는 멤버 함수로 비교할 수도 있는데 compare는 연산자에 비해 문자열의 일부만을 비교할 수 있다는 점이 다르다.

int compare(const string& str) const;

int comapre(size_t pos, size_t num, const string& str) const;

int compare(size_t pos, zie_t num const string& str, size_t off, size_t count) const;

  • 예제 compare

#include <iostream>

#include <string>

using namespace std;

   

void main()

{

string s1 = "aaa";

string s2 = "bbb";

   

cout << (s1 == s1 ? "같다" : "다르다") << endl;

cout << (s1 == s2 ? "같다" : "다르다") << endl;

cout << (s1 > s2 ? "크다" : "작다") << endl;

   

string s3("1234567");

string s4("1234999");

cout << (s3.compare(s4) == 0 ? "같다" : "다르다" ) << endl;

cout << (s3.compare(0, 4, s4, 0, 4) == 0 ? "같다" : "다르다") << endl;

   

string s5("hongkildong");

cout << (s5 == "hongkildong" ? "같다" : "다르다") << endl;

}

여러 가지 다양한 검색 함수가 준비되어 있는데 가장 기본적인 검색 함수는 find이다.

size_t find(char ch, size_t off=0) const;

size_t find(const char* ptr, size_t off=0) const;

size_t find(const char* ptr, size_t off=0, size_t count) const;

size_t find(const string& str, size_t off=0) const;

  • 예제 stringfind

#include <iostream>

#include <string>

using namespace std;

   

void main()

{

string s1("string class find function");

string s2("func");

   

cout << "i:" << s1.find('i') << "번째" << endl;

cout << "i:" << s1.find('i',10) << "번째" << endl;

   

cout << "ass:" << s1.find("ass") << "번째" << endl;

cout << "finding의 앞4:" << s1.find("finding", 0, 4) << "번째" << endl;

cout << "kiss:" << s1.find("kiss") << "번째" << endl;

cout << s2 << ':' << s1.find(s2) <<"번째" << endl;

}

find는 가장 기본적인 검색 함수이며 이외에 검색 방향과 포함 문자 검색, 비포함 문자검색 등의 다양한 함수들이 다양한 원형으로 제공된다.

  • 예제 stringfind2

#include <iostream>

#include <string>

using namespace std;

   

void main()

{

string s1("starcraft");

string s2("123abc456");

string moum("aeiou");

string num("0123456789");

   

cout << "순방향 t:" << s1.find('t') << "번째" << endl;

cout << "역방향 t:" << s1.rfind('t') << "번째" << endl;

cout << "역방향 cra:" << s1.rfind("cra") << "번째" << endl;

cout << "최초의 모음" << s1.find_first_of(moum) << "번째" << endl;

cout << "최후의 모음" << s1.find_last_of(moum) << "번째" << endl;

cout << "최초의 비슷자" << s2.find_first_not_of(num) << "번째" << endl;

cout << "최후의 비슷자" << s2.find_last_not_of(num) << "번째" << endl;

}

36-3 auto_ptr

가. 자동화된 파괴

C++의 클래스는 소멸자라는 특별한 함술를 가지는데 이 함수는 객체가 파괴될 때 자동으로 호출된다. 그래서 객체가 동적으로 메모리를 할당하거나 시스템 자원을 사용하더라도 소멸자에 정리 코드를 작성해 놓으면 별도의 조치가 없더라도 객체가 사라질 때 해제 작업을 하도록 되어 있다. 소멸자의 이런 동작은 굉장히 편리한데 지역 객체일 경우 함수 안에서 마음대로 만들어 쓰다가 그냥 나가기만 하면된다. 범윌르 벗어난 변수는 스택에서 제거되며 이때 객체의 소멸자가 호출되어 자신이 사용하던 자원을 알아서 정리하는 것이다.

string 객체는 버퍼를 동적으로 할당해서 관리하는데 개발자가 신경쓰지 안항도 이 메모리는 자동으로 회수된다. 그러나 소멸자는 스택에 정적으로 할당된 객체에 대해서만 동작하며 동적으로 할당한 메모리에 대해서는 책임지지 않는 문제점이 있다.

  • 예제 dynalloc

#include <iostream>

using namespace std;

   

void main()

{

double *rate;

   

rate = new double;

*rate = 3.1415;

cout << *rate << endl;

// delete rate;

}

rate가 가리키는 메모리는 자동으로 해제되지 않는데 동적으로 할당했다는 것은 필요할 때까지 쓰겠다는 의사 표현이므로 직접 해제하기 전까지는 힙에 계속 남아있는다

메모리 관리 원칙에 의해 한 번 할당한 메모리는 해제할 때까지 다른 용도로 사용되지 않으므로 명시적으로 delete를 호출해야만 해제된다. 그래서 동적으로 할당한 메모리는 반드시 대응되는 해제 코드(free, delete)로 해제해야 한다. 위 예제처럼 delete 호출문이 주석으로 처리되었는데 이렇게 되면 할당한 메모리는 더 이상 사용할 수 없는 메모리 누수(Memory Leak)가 발생할 것이다. 동적으로 할당한 메모리는 이름이 없으므로 핑ㄴ터를 잃어버리면 더 이상 참조할 수 없고 해제하지도 못한다.

이런 식의 메모리 누수는 양이 많지 않을 경우 당장은 별 문제가 되지 않으며 컴파일 중에 에러가 나는 것도 아니다. 그러나 오랫동안 실행되는 프로그램은 시스템 자원을 야금야금 갉아 먹으므로 언젠가는 말썽을 부릴 것이다. 멀티태스킹 환경에서 메모리 누수는 자신뿐만 아니라 같이 실행되는 다른 프로그램에도 피해를 끼친다는 점에서 심각하다.

단순 포인터는 소멸자를 가지지 않기 때문에 C++의 소멸자로는 이 문제를 제대로 해결할 수 없다.

포인터 변수만 해제될 뿐이지 포인터가 가리키는 메모리는 해제되지 않는다. 이런 문제를 해결하기 위해 만들어진 것이 바로 auto_ptr이다. auto_ptr은 동적으로 할당된 메모리도 자동으로 해제하는 기능을 가지는 포인터의 래퍼 클래스이다. auto_ptr의 소멸자에 포인터 해제 코드를 작성하면 어떤 경우라도 안전한 해제를 보장할 수 있다.

  • 예제 auto_ptr

#include <iostream>

#include <memory>

using namespace std;

   

void main()

{

auto_ptr<double> rate(new double);

   

*rate = 3.1415;

cout << *rate << endl;

}

auto_ptr 템플릿은 memory 헤더 파일에 정의되어 있으며 std 네임스페이스에 선언되어 있다.

template<typename T> class auto_ptr

포인터가 가리키는 대상체의 타입 T를 인수로 받아들이며 T*형의 포인터를 대신 관리한다. 생성자로 포인터를 전달하면 이 포인터를 가지고 있다가 소멸자에서 delete로 해제하므로 포인터뿐만 아니라 포인터가 가리키는 메모리도 자동으로 해제된다.

auto_ptr은 이 포인털르 내부 멤버 변수에 저장해놓고 *, ->, = 등 포인터에 사용하는 대부분의 연산자를 오버로딩하여 이 객체에 대한 모든 연산을 내부 포인터에 대한 연산으로 중계하는 역할을 한다. 그래서 rate를 마치 double형의 포인터인 것처럼 사용할 수 있다. *연산자를 적용하면 동적으로 할당된 메모리에 대해 *연산자가 적용되어 이 값을 읽거나 변경할 수 있다. 래퍼이므로 래핑한 대상을 그대로 흉내내는 것이다. rate의 소멸자에서는 delete를 자동으로 호출하므로 함수가 끝날 때 rate를 해제할 필요가 없으며 해제 되지도 않는다. delete rate 코드를 함수 끝에 작성하면 컴파일 에러로 처리하는데 rate 객체 자체는 포인터가 아니기 때문이다. 단순 포인터는 소멸자가 없지만 auto_ptr은 클래스이므로 소멸자가 호출된다.

  • 예제 dynstring

#include <iostream>

#include <string>

using namespace std;

   

void main()

{

string *pStr = new string("autoptr string");

   

cout << *pStr << endl;

// delete pStr;

}

string 객체는 동적으로 할당되었으므로 함수가 종료될 때 자동으로 파괴되지 않으며 예외 처리 구문에 의해 강제 종료 될때도 마찬가지이다. 이 문제도 auto_ptr을 사용하면 해결할 수 있다.

  • 예제 autostring

#include <iostream>

#include <string>

#include <memory>

using namespace std;

   

void main()

{

auto_ptr<string> pStr(new string("autoptr test"));

   

cout << *pStr << endl;

}

string 대상체를 가리키는 auto_ptr 객체 pStr을 선언하고 새로운 string 객체를 동적으로 할당한 번지를 생성자로 전달했다. 이때 pStr의 메모리 내부는 아마도 다음과 같은 모양이 될 것이다.

main 함수가 종료되면 지역 객체 pStr이 파괴되며 이 과정에서 pStr의 소멸자가 호출된다. 소멸자는 내부적으로 유지하고 있는 포인터를 delete한다. 삭제되는 대상이 string 객체이므로 이 과정에서 string의 소멸자가 호출되며 문자열 버퍼도 정리된다. 설사 main이 비정상적으로 종료되더라도 정리 코드가 자동으로 실행되므로 메모리 누수는 발생하지 않는다.

동적으로 메모리를 할당하거나 객체를 생성할 때는 auto_ptr 템플릿을 사용하면 확실히 안전하기는 하다. 그러나 단순 포인터를 쓰는 것에 비해 다소 번거롭다는 단점이 있다. 자신이 책임지고 해제한다거나 예외가 발생할 가능성이 전혀 없다면 auto_ptr을 쓰지 않아도 상관없다.

나. auto_ptr의 내부

auto_ptr 템플릿은 포인터를 클래스로 감싸서 소멸자가 자동으로 해제할 수 있는 포인터의 래퍼 클래스라고 할 수 있다. 자동으로 파괴된다는 것 외엔느 모든 동작이 래핑된 포인터와 동일한데 이것이 어떻게 가능한지 연구해 보자.

  • 예제 myptr

#include <iostream>

#include <string>

using namespace std;

   

template<typename T>

class myptr

{

private:

T *p;

public:

explicit myptr(T *ap) : p(ap) {}

~myptr() { delete p; }

T& operator *() const { return *p; }

T* operator ->() const { return p; }

};

   

void main()

{

myptr<string> pStr(new string("autoptr test"));

   

cout << *pStr << endl;

cout << "길이 = " << pStr->size() << endl;

}

myptr은 인수로 전달된 대상체 T에 대한 포인터를 감싸는 래퍼 클래스이다. 클래스 템플릿이므로 임의 타입의 포인터를 래핑할 수 있다. 멤버 변수 T*형의 p를 선언하고 있는데 이 포인터는 생성자에서 초기화된다. main에서 myptr<string> 타입의 pStr을 선언하고 new string문으로 새로운 string 객체를 동적 할당하여 그 포인터를 생성자로 전달했다. 이렇게 되면 myptr의 멤버 p는 동적 할당된 string 객체를 가리킬 것이다.

이 상태에서 *연산자로 myptr을 읽으면 이 연산자가 *p를 리턴한다. 그래서 myptr에 가해지는 연산은 p가 가리키는 객체, 그러니까 이 예제의 경우 동적할당된 string 객체를 대상으로 하게 된다.*pStr을 읽으면 "autoptr test"라는 문자열이 읽혀질 것이다. 멤버 참조 연산자인 ->도 포인터를 리턴하도록 되어 있으므로 이 연산자로 래핑된 포인터가 가리키는 객체의 멤버를 바로 참조할 수 있다. main이 종료될 때 myptr의 소멸자가 호출되고 여기서 delete p;를 함으로써 p객체 자체와 p가 사용하는 부가 메모리까지도 자동으로 정리되는 것이다.

생성자는 explicit로 선언되어 있어 명시적인 변환만 허용하는데 암시적인 변환까지 허용할 경우 다음과 같은 코드도 이상없이 컴파일 되어 문제가 될 수 있다.

myptr<int> mpi(new int);

int i, *pi = &i;

mpi = pi;

생성자가 explicit가 아니라면 정수형 포인터 변수 pi로부터 임시 myptr 객체를 생성한 후 이 객체를 mpi에 그대로 대입해 버릴 것이다. myptr<int>와 int* 반드시 명시적으로 생성자의 인수로 넘길 때만 이 포인터를 받아 들여야 한다.

다음은 auto_ptr 템플릿을 사용할 때의 일반적인 주의 사항과 한계에 대해 알아보자.

auto_ptr<int> api(new int(1234); // 정상적인 코드

정수형 포인터를 래핑하는 api에 정수형 변수를 할당해서 전달했으므로 문제가 없다. 그러나 다음과 같이 정적으로 할당한 변수의 번지는 전달할 수 없다.

int i = 1234;

auto_ptr<int> api(&i);

왜냐하면 auto_ptr의 소멸자는 무조건 delete 연산자로 포인터를 삭제하도록 되어 있는데 위 코드의 i 변수는 힙에 할당된 것이 아니라 스택에 생성된 것이므로 해제할 수 없는 것이다. int i, *pi = &i; delete pi; 코드를 순서대로 실행했을 때 에러가 발생하는 것도 똑같은 이유이다. 다음 코드도 불가능하다.

auto_ptr<int> api((int*)malloc(sizoef(int));

malloc은 free와 짝이므로 delete로 해제할 수 없다. 자신이 래핑하고 있는 포인터가 new에 의해 할당되었다고 가정하는 것이다. 그래서 다음 코드도 제대로 동작하지 않는다.

auto_ptr<int> api(new int[10]);

auto_ptr객체끼리 대입했을 때 두 개의 대상을 같은 객체가 가리키는 상황이 될 수 있다. 이렇게 되면 두 객체가 개별적으로 해제될 때 이 중 해제에 의한 문제가 발생할 것이다. auto_ptr은 대입할 때 우변 객체가 래핑하고 있는 포인터의 소유권을 포기하고 자신을 스스로 무효화함으로써 이중해제의 위험을 피하며 delete연산자는 NULL 포인터에 대해 아무런 동작도 하지 않음으로써 무효화된 객체도 별 이상 없이 디폴트 처리한다.

그러나 이 방법은 대입 연산에 의해 한 쪽의 auto_ptr 객체가 무효화되며 두 객체가 한 대상을 가리키지 못한다는 논리적인 취약점이 있다. 그래서 좀 더 똑똑한 래퍼는 같은 대상을 가리키는 회수인 참조 카운트를 유지하며 객체가 해제될 때 카운트만 1감소하고 카운트가 0이 될 때 실제 객체를 해제하는 방법을 쓰기도 한다. 이런식으로 동작하는 포인터를 스마트 포인터(Smart Pointer)라고 하는데 auto_ptr보다는 한 단계 더 발전된 개념이다.

반응형

'책정리 > 혼자 연구하는 C,C++ 2' 카테고리의 다른 글

38장 함수 객체  (0) 2015.03.13
37장 STL 개요  (0) 2015.03.12
34장 네임 스페이스  (0) 2015.03.10
33장 타입 정보  (0) 2015.03.09
32장 예외 처리  (0) 2015.03.07