본문으로 바로가기
반응형

표준 라이브러리

출력 스트림

표준 출력 객체인 cout은 '<<' 연산자로 데이터를 보내 출력한다. 모든 기본 타입에 대해 '<<' 연산자를 다 오버로딩해 두어 타입에 상관없이 보내기만하면 된다. 출력 후에 cout 객체의 레퍼런스를 다시 리턴하므로 연쇄적으로 출력할 수 없다. 정수든 실수든 '<<' 연산자로 보내고 개행할 때는 endl을 보낸다.

 

'<<' 연산자는 원래 시프트 연산자인데 모양이 데이터를 전송하는 형태여서 출력용으로 정의했다. 모양이 직관적이라 사용하기 쉽지만 연산자를 오버로딩한다고해서 본래의 우선순위가 바뀌는 것은 아니어서 주의가 필요하다.

 

콘솔은 문자열을 표시하는 장치이므로 cout은 출력 대상을 내부적인 변환 규칙에 따라 평이만 문자열로 변환한다. 예를들어 정수 123은 문자열 "123"으로 출력된다. 변환 규칙에 변화를 주고싶다면 조정자를 사용한다. 정수의 진법을 변경하는 예제를 살펴보자.

// coutradix.cpp
#include <iostream>
using namespace std;

int main()
{
	int i = 1234;

	hex(cout);
	cout << i << endl;

	cout << "8진수  : " << oct << i << endl;
	cout << "16진수 :" << hex << i << endl;
	cout << "10진수 :" << dec << i << endl;
}

별다른 지정이 없다면 10진수로 출력되지만 hex, oct, dec 등의 함수로 출력 방식을 변경하면 진법이 바뀐다.

함수를 호출하는 대신 함수 포인터를 cout 객체로 보내도 효과는 가다. 진법이 변경되면 이후부터 지정한 진법으로 계속 출력된다. 진법 지정은 정수에만 유효하며 실수나 문자열에는 아무 영향을 주지 않는다. 다음은 출력 폭을 지정하는 조정자이다.

 

// coutwidth.cpp
#include <iostream>
using namespace std;

int 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 함수로 지정한다. i는 4자리 정수지만 width(10)을 호출한 후 출력하면 10자리를 차지한다.

이 폭은 최소 폭이지 강제 폭은 아니어서 데이터의 길이보다 작아도 데이터를 자르지는 않는다. width는 직후의 출력에 딱 한 번만 적용되며 원래 설정으로 복귀한다.

 

fill 함수는 출력 폭이 더 길 때 공백을 어떤 문자로 채울 것인가를 지정한다. left, right 조정자는 데이터를 공백의 왼쪽, 오른쪽으로 정렬한다. internal 정렬은 부호나 진법 표시는 왼쪽에 출력하고 숫자는 오른쪽에 출력한다.

 

 

파일 입출력

파일 입출력 스트림인 basic_i(o)fstream은 콘솔용 스트림으로부터 상속되며 >>, << 연산자와 조정자, 멤버 함수를 상속받는다. 다만 입출력 대상이 파일이므로 열고 닫는 동작과 섬세한 에러 처리가 필요하다는 것만 다르다. 파일 입출력 클래스도 템플릿이며 fstream 헤더 파일에 특수화 클래스가 정의되어있다.

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

첫번째 인수로 경로를 주고 두번째 인수로 모드를 주되 객체에 따라 디폴트 모드가 무난하게 설정되어있다.

스트림의 입출력 모드
ios_base::out : 출력용으로 파일을 연다.
ios_base::in :  입력용으로 파일을 연다.
ios_base::app : 파일 끝에 데이터를 덧붙인다. 데이터를 추가하는 것만 가능하다.
ios_base::ate :  파일을 열자마자 파일 끝으로 FP를 보낸다. FP를 임의 위치로 옮길 수 있다.
ios_base::trunc : 파일이 이미 존재할 경우 크기를 0으로 만든다.
ios_base::binary : 이진 파일 모드로 연다.

오픈 성공 여부는 is_open 함수로 조사하며 다 사용한 파일은 close 함수로 닫는다. 

함수의 형식만 다를뿐 입출력과정은 C와 거의 유사하다. 다음 예제는 텍스트 파일로 문자열과 정수를 출력한다.

// cppfilewrite.cpp
#include <iostream>
#include <fstream>
using namespace std;

int main()
{
	ofstream f;
	f.open("C:\\cpptemp\\cpptest.txt");
	if (f.is_open())
	{
		f << "String" << 1234 << endl;
		f.close();
	}
	else
	{
		cout << "파일 열기 실패" << endl;
	}
}

txt 파일에 string1234 가 저장되어 있다

ofstream 객체를 선언하고 open 함수로 파일을 열었다. 두 단계를 거치는 대신 생성자의 인수로 파일 경로를 바로 전달해도 상관없다.

 

파일 입출력은 에러 발생 가능성이 높아 항상 성공 여부를 점검해야 한다. 오픈에 성공하면 << 연산자로 데이터를 출력한다. 콘솔과 같은 방법으로 파일에 출력하며 완료 후 close 함수로 파일을 닫는다. 

다음 예제는 파일로부터 데이터를 다시 읽는다.

// cppfileread.cpp
#include <iostream>
#include <fstream>
using namespace std;

int main()
{
	ifstream f;
	char str[128];
	int i;

	f.open("C:\\cpptemp\\cpptest.txt");
	if (f.is_open())
	{
		f >> str >> i;
		cout << str << i << endl;
		f.close();
	}
	else
	{
		cout << "파일 열기 실패" << endl;
	}
}

입력 객체를 생성하고 open 함수로 열었다. 오픈에 성공하면 콘솔에서 입력받듯이 >> 연산자로 변수에 대입한다.

문자열과 정수를 입력받아 제대로 읽었는지 콘솔로 다시 출력했다. 앞 예제르 먼저 실행했다면 cpptest.txt 파일에 문자열과 정수가 저장되어있을 것이다.

 

이진모드로 대량의 데이터를 입출력할 수도 있고 FP를 옮겨가며 임의의 위치를 읽을 수도 있다. 일반적으로 필요한 파일 입출력 기능은 다 제공하며 콘솔과 똑같은 방법으로 사용할 수 있어 쉽다. 그러나 범용적인 기능만 제공하다보니 아무래도 운영체제의 입출력 함수보다는 성능이 떨어진다.

 

 

string

문자열 클래스

C/C++에서는 문자열은 기본 타입이 아니며 배열로 문자의 집합을 표현한다. 배열로도 문자열을 관리할 수 있지만 귀찮고 경계를 점검할 수 있어 안정성을 위협할 수도 있다. 다행히 클래스를 활용하면 더 고급진 문자열 타입을 만들 수 있다.

 

C++이 라이브러리 차원에서 제공하는 string 문자열 클래스는 쓰기 편하고 호환성과 이식성을 걱정할 필요없이 자유롭게 사용할 수 있다. std 네임스페이스에 포함되어 있으며 string 헤더파일에 선언되어있다. 일반화를 위해 템플릿으로 정의되어 있다.

template<class _Elem, class _Traits = char_traits<_Elem>, class _Ax = allocator<_Elem>>
class basic_string{멤버 목록};

타입 인수로 문자 코드, 문자열의 형태는 물론이고 메모리 관리 방식까지 선택할 수 있어 지원 범위가 광범위하다. 디폴트 인수를 사용하면 힙에 생성되는 널종료 방식의 문자열이 된다. 현실적으로는 char, wchar_t 타입의 문자열을 주로 사용하는데 이 두 버전에 대해 특수화 클래스가 정의되어 있다.

typedef basic_string<char> string;
typedef basic_string<wchar_t> wstring;

string은 ANSI 문자열이며 wstring은 유니코드 문자열이다. 문자 코드만 다를뿐 같은 템플릿으로부터 생성된 클래스여서 내부 구조가 사용하는 방식은 같다.

 

클래스를 처음 연구할때는 생성자부터 봐야한다. string은 문자열을 만들 수 있는 모든 방법에 대해 생성자를 제공한다. 디폴트 생성자, 복사 생성자, 대입 연산자 등 온전한 타입이 되기 위한 모든 기능이 정의되어 있다.

자주 사용되는 생성자는 다음과 같다.

 

string의 생성자

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의 문자로 구성된 문자열 생성

문자열 리터럴로 생성할 수도 있고 다른 문자열의 일부를 취하거나 반복되는 문자로도 생성 가능하다.

객체의 세계에서는 사용자가 필요를 느끼는 모든 기능이 다 정의되어 있다고 보면 거의 틀림없다.

다음 예제는 각 방식으로 문자열을 생성한다.

// stringctor.cpp
#include <iostream>
#include <string>
using namespace std;

int main()
{
	string s1("test");
	string s2(s1);
	string s3;
	string s4(10, 'S');
	string s5("very nice day", 8);
	const char* str = "abcdefghijklmnopqrstuvwxyz";
	string s6(str + 5, str + 10);  // 5번째부터 10번째까지 문자열을 s6에 저장

	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 방식은 문자열 리터럴로부터 초기화하는 방식으로, 가장 보편적이다.

s2는 복사 생성자로 다른 문자열의 사본을 만드는데 깊은 복사를 하므로 생성 후에는 개별적인 문자열이다.

s3은 디폴트 생성자로 빈 문자열로 초기화되지만 실행 중에 다른 문자열을 대입받거나 연결할 수 있다.

s4는 같은 문자를 원하는 횟수만큼 반복한다.

s5는 문자열 상수에서 일부만을 취한다.

s6은 다른 문자열의 일정 범위에서 문자열을 추출한다. 반복자로 범위를 표현하는데 일종의 문자열 포인터이다.

알파벳이 저장된 str에서 5~10범위를 취해 새로운 문자열을 만든다.

 

문자열 객체는 내용을 저장하기 위해 메모리를 할당하고 범위를 벗어날 때 소멸자가 메모리를 자동으로 정리하도록 되어있어 별다른 정리를 할 필요는 없고 쓰다가 버리면된다.

 

char *str = "hello";   : 리터럴, 값 변경이 안됨(그래서 앞에 const를 붙여야함)

char str[] = "World"; : 배열로써 값 변경이 가능

 

 

메모리 관리

string 객체는 가변적인 길이의 문자열 저장을 위해 힙에 메모리를 할당한다. 문자열이 아무리 길어도 버퍼 길이를 자동으로 관리하며 문자열이 늘어나면 버퍼를 재할당하고 파괴될 때 알아서 정리한다. 생성, 연결, 대입 등 모든 동작에 대해 메모리를 섬세하게 관리하고 있어 배열 경계를 넘어설 위험이 없다.

 

모든 것이 자동화되어 있지만 너무 빈번한 재할당은 성능을 떨어뜨리는 원인이 된다. 그래서 사용자가 직접 메모리를 제어할 수 있도록 버퍼의 길이를 조사하고 관리하는 함수를 제공한다. 다음 예제를 통해 메모리 관리 방식을 들여다 보자.

// stringsize.cpp
#include <iostream>
#include <string>
using namespace std;

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

	cout << s.at(0) << endl;  // 배열의 index를 찾아가려면 at을 사용

	s.clear();
	cout << s << endl;    // clear했기때문에 아무것도 출력안됨
	cout << s << "길이 = " << s.size() << ", 할당 크기 = " << s.capacity() << endl;  
	// size길이는 없어졌지만, capacity의 메모리할당은 남아있음
}

size와 length는 똑같은 함수이며 객체에 저장된 문자열의 길이를 조사한다. capacity 함수는 객체가 실제 할당한 메모리 양을 조사하는데 늘어날 때를 대비하여 여유분을 더 할당해 놓기 때문에 실제 문자열보다 항상 더 크다. max_size 함수는 가능한 최대 길이를 조사하는데 32비트 용량인 42억이되어 실질적으로 무한한 셈이다.

 

resiz 함수는 할당량을 강제로 변경한다. 현재 문자열보다 더 작은 크기를 지정하면 뒤쪽을 잘라버리고 더 크면 뒤쪽을 NULL문자로 채우되 두 번째 인수로 채울 문자를 지정할 수 있다. reserve 함수는 여유분까지 고려하여 메모리를 미리 확보한다. 문자열이 자주 변경된다면 충분한 길이로 늘려 재할당 횟수를 줄여야 성능이 향상된다.

void clear();
bool empty() const;

이 두 함수는 문자열을 비우거나 비어있는지 조사한다. clear를 호출하는 것은 ""를 대입하는 것과 같다.

empty는 문자열의 길이가 0일때 true를 리턴한다.

 

string은 모든 면에서 문자 배열보다 우월하다. 그러나 string을 인식하지 못하는 구형 함수는 아직도 문자열 포인터를 요구한다. 예를들어 strstr 함수를 호출하고 싶다거나 fwrite 함수로 문자열을 파일로 출력할 때이다. 

이때는 data 함수나 c_str 함수로 단순 문자열을 얻는다.

// chararray.cpp
#include <iostream>
#include <string>
using namespace std;

int main()
{
	string s("char array");

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

	char str[128];
	strcpy_s(str, s.c_str());
	printf("str = %s\n", str);
}

string 객체에서 두 함수의 리턴값은 같지만 약간의 차이점은 있다. 문자열을 표현하는 basic_string 템플릿은 다양한 문자열 형태를 제공하며 꼭 널종료 문자열만 가능한 것은 아니다. data 함수는 객체의 내부 데이터를 그대로 리턴하므로 다른 형식의 문자열일 수 있다. 반면 c_str은 널종료 문자열이 아닌 경우 사본을 복사하여 널종료 문자열을 만들어 리턴한다.

 

C스타일의 널종료 문자열을 얻고 싶으면 c_str함수를 사용하는 것이 더 원칙저이다. string 객체의 문자열을 문자 배열로 복사하고 싶다면 충분한 길이의 배열을 선언하고 strcpy 함수로 문자열 객체의 c_str 함수가 리턴한 포인터를 복사한다.

 

 

입출력

string 객체를 화면으로 출력할때는 cout << 로 보낸다. string 헤더 파일에 cout과 string 객체를 인수로 취하는 << 전역 연산자 함수가 오버로딩되어 있어 기본형과 똑같은 방법으로 출력한다. 입력도 마찬가지고 cin과 >> 연산자를 사용하되 >>는 공백을 구분자로만 인식하여 한 단어만 입력받을 수 있다. 공백을 포함하여 한 줄을 다 입력받으려면 getline 전역 함수를 사용한다.

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

int main()
{
	string name, addr;

	cout << "이름을 입력하시오 : ";
	cin >> name;
	cout << "입력한 이름은 " << name << "입니다." << endl;
	cin.ignore();  // 버퍼를 비워줌
	cout << "주소를 입력하시오 : ";
	getline(cin, addr);  // 띄워쓰기를 지원하는 멤버함수
	cout << "입력한 주소는 " << addr << "입니다." << endl;
}

한 단어인 이름은 cin >> 으로 입력받았으며 한 행인 주소는 getline 함수로 받는다. 버퍼 길이를 걱정할 필요없이 아무리 긴 문자열도 문제없이 입력받을 수 있다. cin은 입력 후 개행 코드를 버퍼에 남겨두는 버릇이 있어 새로운 문자열을 입력받으려면 ignore 함수를 호출하여 버퍼를 비워야한다.

 

문자열의 개별 문자를 액세스할때는 at함수나 '[]' 연산자를 사용한다. 인수로 첨자를 전달하면 이 위치의 문자를 리턴한다. 리턴값은 레퍼런스이므로 좌변값이며 문자열 객체가 상수가 아니라면 특정 위치의 문자를 변경할 수 있다.

char& operator[](size_type _Off)
char& at(size_type _Off)

at 함수보다는 [] 연산자가 배열처럼 객체를 사용할 수 있어 직관적이다. '[]' 는 배열의 범위를 점검하지 않아 위험하지만 at 함수는 범위를 넘어선 첨자를 사용하면 out_of_range 예외를 일으킨다는 면에서 안전하다. 그러나 매번 첨자의 유효성을 점검하므로 속도는 조금 느리다. 첨자가 유효하다는 확신이 있다면 '[]' 연산자가 더 유리하다.

// stringat.cpp
#include <iostream>
#include <string>
using namespace std;

int main()
{
	string s("korea");
	size_t len;

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

	cout << endl;
	s[0] = 'c';

	for (int i = 0; i < len; i++)
	{
		cout << s.at(i);
	}
	cout << endl;
}

size 함수로 길이를 조사하고 루프를 돌며 s[i] 문자를 읽어 출력했다. 개별 문자가 순서대로 출력되니 문자열 전체를 출력하는 것과 같다. s[0]를 좌변에 두면 문자를 변경할 수도 있다. 변경 후 다시 루프를 돌며 at[i]로 개별 문자를 읽어 다시 출력했다. 이 경우 for 루프가 문자열 길이까지만 정확히 반복하여 첨자는 항상 유효하므로 '[]' 연산자를 쓰는 것이 더 합리적이다.

 

 

대입 및 연결

string 클래스는 문자열을 조작하는 여러 가지 연산자와 함수를 제공한다. 필요한 거의 모든 기능이 다 정의되어 있고 원형이 상식적이므로 요약적으로 알아보자. 대입연산자는 다음 세가지가 오버로딩되어있다.

string& operator=(char ch);
string& operator=(const char* str);
string& operator=(const string& other);

개별 문자, 문자열 리터럴, string 객체를 실행 중에 대입받는다. 문자열 뒤에 연결할 때는 '+=' 연산자를 사용하며 대입 연산자와 마찬가지로 세 가지 타입을 모두 연결할 수 있다. 대입이나 연결할 때도 메모리는 자동으로 관리되므로 길이는 걱정하지 않아도 된다.

// stringEqualplus.cpp
#include <iostream>
#include <string>
using namespace std;

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

	cout << s1 << endl;
	s1 += '!';
	cout << s1 << endl;

	string s3;
	s3 = "s1:" + s1 + "s2:" + '.';
	cout << s3 << endl;
}

대입, 연결 연산자 모두 string 객체를 리턴하여 연쇄적인 연산이 가능하다. '+' 연산자는 프렌드로 선언된 전역 함수이며 임의의 타입을 연쇄적으로 연결한다. 여러 개의 문자열을 하나로 합칠때 한 줄로 연결할 수 있어 편리하고 직관적이지만 내부적으로 메모리 재할당이 빈번해 성능은 떨어진다.

 

연산자는 항상 피연산자 전체를 대상으로 하여 전부 대입하거나 전부 연결한다. 다른 문자열의 일부만 취하고 싶을때는 연산자를 쓸 수 없으며 함수로 범위를 밝혀야한다. 다음 두 함수는 off 위치에서 count 개수 만큼만 대입하거나 연결한다.

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

'=' 연산자가 strcpy에 대응된다면 assign 함수는 strncpy에 대응된다. 다음 두 예제는 두 개의 문자열 객체로부터 일부를 대입하고 연결한다.

// AssignAppend.cpp
#include <iostream>
#include <string>
using namespace std;

int 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;
}

s3는 디폴트 생성자에 의해 빈 문자열로 생성되었다가 s1의 일부를 대입받고 s2의 일부를 연결한다.

 

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

size_type copy(value_type* _Ptr, size_type _Count, size_type _Off = 0) const;
void swap(basic_string& _Str);

copy는 _Off 위치에서 _Count 개수만큼의 문자를 복사하며 널종료 문자는 붙이지 않는다. 문자열을 대입받을 _Ptr 배열은 충분한 길이여야 한다.

 

 

삽입과 삭제

문자열 중간에 다른 문자나 문자열을 삽입할때는 insert 함수를 사용한다. 삽입 대상에 따라 여러벌의 함수가 제공되는데 대표적인 몇가지만 보인다. 전체 함수 목록은 항상 레퍼런스를 참조하자.

string& insert(size_t pos, const char* ptr, size_t count);
string& insert(size_t pos, const string& str, int off, int count);
string& insert(size_t pos, int count, char ch);

첫번째 인수로 삽입할 위치를 전달하고 두번째 인수는 삽입할 대상을 전달한다. 문자열 리터럴이나 string 객체, 같은 문자의 반복을 삽입할 수 있다. 문자열 일부를 삭제할때는 다음 함수를 사용한다.

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

위치와 개수를 전달하면 중간의 문자열이 삭제되며 뒤쪽의 문자들이 앞쪽으로 이동한다.

// stringinsert.cpp
#include <iostream>
#include <string>
using namespace std;

int main()
{
	string s1("123456790");
	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);

pos 위치에서 num개 까지의 문자열을 ptr로 대체한다.

원본과 대체할 문자열의 길이가 꼭 같은 필요는 없으며 더 긴 문자열로 대체해도 메모리가 자동으로 늘어난다.

// stringreplace.cpp
#include <iostream>
#include <string>
using namespace std;

int main()
{
	string s1 = "독도는 느그땅";

	cout << s1 << endl;
	s1.replace(7, 4, "우리");
	cout << s1 << endl;
}

s1의 세번째 문자에서부터 길이 4만큼을 취해 s2 문자열을 새로 만들었다.

 

비교와 검색

문자열끼리 상등, 대소 비교할때는 관계 연산자를 사용한다. 기본 타입과 같은 똑같은 방식으로 비교할 수 있도록 모든 연산자가 다 오버로딩되어있다. 연산자는 항상 문자열 전체를 대상으로 비교한다. 

// compare.cpp
#include <iostream>
#include <string>
using namespace std;

int main()
{
	string s1("aaa");
	string s2("bbb");

	cout << (s1 == s1 ? "같다" : "다르다") << endl;
	cout << (s1 == s2 ? "같다" : "다르다") << endl;
	cout << (s1 > s1 ? "크다" : "안크다") << 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;
}

문자열 비교는 사전순이다. 비교 연산은 좌우변의 순서가 중요하지않아 s1 == s2로 비교하든 s2 == s1으로 비교하든 결과는 같다. '==' 연산자가 전역으로 선언되어있어 문자열 리터럴과 비교할때도 "hongkildong" == s5 처럼 상수를 좌변에 쓸 수 있다.

 

다음 함수는 string 객체에서 부분 문자열이나 특정 문자열을 찾는다. 여러가지 검색 함수가 제공되는데 가장 기본적인 함수는 find이며 여러 벌로 오버로딩되어있다.

 

문자, 문자열, string 객체를 off위치에서부터 검색하며 검색할 길이도 지정할 수 있다. 발견되면 그 첨자가 리턴되며 발견되지 않으면 string::npos(-1)를 리턴한다.

// stringfind.cpp
#include <iostream>
#include <string>
using namespace std;

int 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;
}

i 문자를 찾되 선두부터 찾을때와 10번째 위치에서 찾을때 검색 위치가 다르다. 발견된 다음 위치부터 검색을 반복하면 해당 문자의 모든 위치를 다 알아낼 수 있다. 부분 문자열 전체나 부분 문자열의 일부만 검색할 수도 있다. 그외의 검색 함수는 검색 방향 지정, 포함 문자 검색, 비포함 문자 검색 등의 기능을 제공한다.

 

 

auto_ptr

자동화된 소멸

객체가 파괴될 때 파괴자가 자동으로 호출하여 마지막 정리 작업을 수행한다. 메모리를 할당했거나 시스템 자원을 사용하더라도 객체가 제거될때까지 파괴자가 알아서 정리해주니 굉장히 편리하다. 지역 객체는 함수 안에서 마음대로 만들어 쓰다가 그냥 나가버리면 나머지는 모두 자동이다.

 

string 객체는 가변 길이의 문자열을 저장하기 위해 버퍼를 동적으로 할당하는데 개발자가 신경쓰지 않아도 이 메모리는 자동으로 회수된다. 그러나 객체에 대해서는 소멸자라는 안전장치가 있지만 동적으로 할당한 메모리는 자동 회수가 되지 않는다.

 

동적할당은 필요할 때까지 쓰겠다는 의사 표현이므로 명시적으로 delete를 호출해야 해제된다. delete 호출을 실수로 빼 먹으면 이때 메모리누수가 발생한다. 포인터를 잃어버리면 동적할당된 메모리는 더 이상 참조할 수 없고 해제도 되지 않기때문에 미아가 되버린다. 이렇게되면 실수에 의한 누수뿐만 아니라 예외 처리 구문이나 비정상적인 상황에 의해 제대로 해제되지 않는다.

 

정상적인 실행 흐름에서는 new, delete가 짝을 이루어 할당, 해제되지만 예외가 발생하면 함수가 강제 종료되어 호출부의 catch 블록으로 점프해 버리므로 뒤쪽의 delete는 실행되지 못한다.  스택되감기에 의해 모든 지역 객체가 자동으로 파괴되지만 지역 객체가 가리키는 메모리까지 해제하는 것은 아니어서 불가피하게 메모리 누수가 발생한다.

 

적은 양의 메모리 누수는 큰 문제가 아닐 수도 있지만 몇 년씩 실행되는 서버 프로그램의 경우는 약간의 누수도 심각하다. 단순 포인터는 소멸자가 없어서 자동으로 소멸이 불가능하다. 이 문제를 해결하기위해 동적 할당되는 포인터를 래핑한 것이 auto_ptr이다. 포인터를 한 번 감싸 파괴시 자동으로 해제한다.

// auto_ptr.cpp
#include <iostream>
#include <memory>
using namespace std;

int main()
{
	auto_ptr<double> rate(new double);

	*rate = 3.1415;
	cout << *rate << endl;
}

rate는 실수형 포인터를 감싸는 레퍼런스 객체이다. 예제는 명시적인 delete 호출이 없지만 자동화된 소멸에의해 메모리 누수가 발생하지 않는다. auto_ptr은 memory 헤더 파일에 정의되어있는 클래스 템플릿이다.

 

단순포인터는 소멸자가 없지만 이 포인터를 감싼 auto_ptr은 객체이므로 소멸자가 자동으로 호출된다.

main 함수가 종료될때 소멸자가 호출되며 여기서 저장해둔 포인터를 delete로 해제한다. 예외에 의해 스택 되감기를 할 때도 소멸자가 잘 호출된다. 기본형 포인터 뿐만아니라 객체에 대한 포인터도 감쌀 수 있다.

// dynstring.cpp
#include <iostream>
#include <string>
#include <memory>
using namespace std;

int main()
{
	auto_ptr<string> pStr(new string("AutoPtr Test"));

	cout << *pStr << endl;
}

string 객체는 대량의 메모리를 소모하는데 동적으로 생성해놓고 소멸시켜주지않으면 시스템 메모리를 계속 차지하고있다. 문자열 객체도 auto_ptr로 감싸 두면 스택의 레퍼런스 객체가 소멸할때 string이 할당한 메모리가 자동으로 해제된다.

main이 종료될때 지역 객체인 pStr이 소멸하면서 소멸자를 호출한다. 소멸자는 내부적으로 저장해둔 string 객체의 포인터를 delete로 해제하며, 이 과정에서 string의 파괴자에 의해 문자열을 저장해둔 버퍼도 정리된다.

 

auto_prt은 포인터를 감싸 소멸자 호출을 보장한다는 면에서 안전하다. 그러나 선언 형식이 복잡해서 다소 번거롭다. 해제 코드를 누락하지 않을 자신이 있거나 예외가 절대 발생하지 않는다면 굳이 auto_prt을 쓸 필요는 없다. 하지만 이런 확신을 가지기 어렵기 때문에 auto_ptr로 감싸 확실한 해제를 보장받는다.

반응형