[C++ 기초 플러스] Chapter 16. (1) string 클래스, 스마트 포인터 템플릿 클래스
Table of Contents
C++ 기초 플러스 책을 읽고 공부한 노트입니다.
string 클래스 #
string
헤더파일을 통해 지원된다.- C 스타일은
string.h
와cstring
이다.
- C 스타일은
string
은 실제로는 템플릿 특수화basic_string<char>
에 대한typedef
이다.
// string 클래스는 템플릿 클래스에 기초한다.
template<class charT, class traits = char_traits<charT>, class Allocator = allocator<charT>>
class basic_string
{
// char 뿐만 아니라 다른형에 기초하는 문자열도 사용할 수 있게 허용한다.
typedef basic_string<char> string;
typedef basic_string<wchar_t> wstring;
typedef basic_string<char16_t> u16string; // C++11
typedef basic_string<char32_t> u32string; // C++11
// ...
};
-
string
생성자들- 메모리와 관련된 선택적 매개변수를 생략했다.
-
(1)
string(const char* s)
string
객체를s
가 지시하는 NBTS로 초기화한다.
-
(2)
string(size_type n, char c)
- 문자
c
로 모두 초기화된 원소n
개의string
객체를 생성한다.
- 문자
-
(3)
string(const string& str)
string
객체를string
객체str
(복사 생성자)로 초기화한다.
-
(4)
string()
- 크기가 0인 디폴트
string
객체를 생성한다 (디폴트 생성자)
- 크기가 0인 디폴트
-
(5)
string(const char* s, size_type n)
string
객체를s
가 자시하는 NBTS로 초기화하되, NBTS의 크기를 초과하더라도n
개의 문자까지 진행한다.
-
(6)
template<class Iter> string(Iter begin, Iter end)
string
객체를begin
과end
- 1 범위에 있는 [begin
,end
) 값들로 초기화한다.begin
과end
는 포인터와 비슷한 역할을 하여 위치를 지정한다. 그 범위는begin
을 포함하고end
는 포함하지 않는end
바로 앞까지를 의미한다.
-
(7)
string(const string& std, size_type pos, size_type n = npos)
string
객체를string
객체str
로 초기화한다.str
에 있는pos
위치에서 시작해서str
의 끝까지 가거나n
문자를 사용하되,str
의 끝을 넘어갈 수 없다.
-
(8)
string(string&& str) noexcept
- (C++11)
string
객체를string
객체str
로 초기화한다.str
은const
가 아니므로 바뀔 수 있다. (move 생성자) - 컴파일러는 경우에 따라 성과를 최적화하기 위해서 복사 생성자 대신 이동 생성자를 사용한다.
- (C++11)
-
(9)
string(initilaizer_list<char> il)
-
(C++11)
string
객체를 초기자 목록il
에 있는 문자로 초기화한다. 따라서 리스트 초기화를 가능하게 한다. -
NBTS
- null byte terminated string; 널 바이트 종료 문자열
-
type_size
string
헤더파일에 정의되어 있는, 시스템마다 다른 정수형이다.string::npos
를 문자열의 최대길이로 정의한다. 일반적으로unsigned char
의 최대값과 같다.
-
#include <iostream>
#include <string>
using namespace std;
int main()
{
string one("Lottery Winner!");
cout << one << endl;
string two(20, '$');
cout << two << endl;
string three(one);
cout << three << endl;
one += " Oops!"; // 오버로딩 +=
cout << one << endl;
two = "Sorry! That was "; // 오버로딩 =
three[0] = 'P'; // 오버로딩 []
string four;
four = two + three; // 오버로딩 =, +
cout << four << endl;
char alls[] = "All's well that ends well";
string five(alls, 20);
cout << five << "!\n";
// 배열의 이름은 주소이다.
// 따라서 alls + 6은 char*형이다. 따라서 Iter가 char*형이 된다.
string six(alls + 6, alls + 10); // 인덱스 6부터 9까지
cout << six << ", ";
// &five[6] 또한 char*형이다.
string six2(&five[6], &five[10]);
cout << six2 << "...\n";
string seven(four, 7, 16); // 인덱스 7부터 16개 문자
cout << seven << " in motion!" << endl;
string nine = { 'L', 'i', 's', 't' };
}
- 입력
// C 스타일 문자열
char info[100];
cin >> info; // 한 단어를 읽는다.
cin.getline(info, 100); // 한 행을 읽되, \n은 내버린다.
cin.get(info, 100); // 한 행을 읽되, \n은 큐에 남겨 둔다.
// string 객체
string stuff;
cin >> stuff; // 한 단어를 읽는다.
getline(cin, stuff); // 한 행을 읽되, \n은 내버린다.
getline()
은 입력을 구분하기 위한 선택적 매개변수를 허용한다.- 차이점은
string
의 경우에는 객체의 크기를 자동으로 조절한다는 것이다.
- 차이점은
// C 스타일 문자열
cin.getline(info, 100, ':'); // :까지 읽고, :는 내버린다.
// string 객체
getline(cin, stuff, ':'); // :까지 읽고, :는 내버린다.
getline()
은 다음 중 하나가 일어나면 종료된다.- (1) 파일의 끝을 만났을 때. 입력 스트림의
eofbit
가 설정된다. - (2) 구분문자(디폴트는
\n
이다. 만약 구분문자가 다른 것이되면\n
은 그냥 일반 문자가 된다)에 도달했을 때. 구분문자는 저장되지 않는다. - (3) 가능한 최대 문자 수(
string::npos
와 대입에 사용할 수 있는 메모리 바이트 수 중 더 적은 것)를 읽었을 때. 입력 스트림의failbit
가 설정된다.
- (1) 파일의 끝을 만났을 때. 입력 스트림의
string
문자열의 크기length()
- 오래된 버전부터 사용해온 것
size()
- STL 호환성을 위해 추가된 것
find()
메서드의 네 가지 변형-
(1)
size_type find(const string& str, size_type pos = 0) const
pos
위치에서 시작해서 처음 나오는str
문자열을 찾는다.
-
(2)
size_type find(const char* s, size_type pos = 0) const
pos
위치에서 시작해서 처음 나오는s
문자열을 찾는다.
-
(3)
size_type find(const char* s, size_type pos = 0, size_type n) const
pos
위치에서 시작해서s
문자열의 처음n
개에 해당하는 부분 문자열을 찾는다.
-
(4)
size_type find(char ch, size_type pos = 0) const
pos
위치에서 시작해서 처음 나오는 문자ch
를 찾는다.
-
찾으면, 첫 문자의 인덱스를 반환한다.
-
찾지 못하면,
string::npos
를 리턴한다.
-
find()
관련 메서드rfind()
- 가장 마지막으로 발생하는 부분 문자열이나 문자를 찾는다.
find_first_of()
- 매개변수 문자열에서 처음 나오는 문자를 찾는다.
find_last_of()
- 매개변수 문자열에서 마지막으로 나오는 문자를 찾는다.
string findHere("ACAB");
int where = findHere.find_first_of("BC"); // 가장 처음 나오는 'C' 인덱스 1
where = findHere.find_last_of("BC"); // 가장 마지막으로 나오는 'B' 인덱스 3
- 자동 크기 조절
- 매번 크기가 늘어날 때마다 새로운 블록을 대입하고 복사하는 것은 비효율적이다.
- 따라서 애초에 실제 문자열보다 훨씬 큰 메모리 블록을 대입한다.
- 그리고, 그 크기를 넘으면 그것의 두 배가 되는 블록을 새로 대입한다.
capacity()
- 현재 블록의 크기를 리턴한다.
reserve(size_t n)
- 블록의 최소한의 크기를 설정한다.
string empty;
string small = "bit";
string larger = "Elephants are a girl's best friend";
cout << "Sizes:\n";
cout << "\tempty: " << empty.size() << endl;
cout << "\tsmall: " << small.size() << endl;
cout << "\tlarger: " << larger.size() << endl << endl;
cout << "Capacities:\n";
cout << "\tempty: " << empty.capacity() << endl;
cout << "\tsmall: " << small.capacity() << endl;
cout << "\tlarger: " << larger.capacity() << endl << endl;
empty.reserve(50);
cout << "Capacity after empty.reserve(50): " << empty.capacity() << endl;
Sizes:
empty: 0
small: 3
larger: 34
Capacities:
empty: 15 // 이 시스템은 15문자의 최소용량을 사용한다.
small: 15
larger: 47
Capacity after empty.reserve(50): 63
c_str()
- C 스타일로 바꾸기
// string에 들어있는 파일명이 필요할 경우.
// open()은 C 스타일 매개변수를 요구한다.
string filename;
cin >> filename;
ofstream fout;
fout.open(filename.c_str());
스마트 포인터 템플릿 클래스 #
- 스마트 포인터가 필요한 이유
- 동적 메모리 할당 후 해제하는 것을 잊어서 메모리 누수가 발생할 수 있다.
- 아래 예제와 같은 상황에서는 해제하는 코드를 작성하려면
try
catch
문을 또 작성해야할 것이다. - 이러한 문제를 해결할 수 있는 더 멋진 해결책이 없을까?
void Remodel(string& str)
{
string* ps = new string(str);
//...
if (weirdThing())
throw exception();
// 지역 변수인 ps가 차지하는 메모리는 블록을 나가면서 해제되지만
// delete ps 하지 않았기 때문에 ps가 가리키는 메모리는 해제되지 않는다!
str = *ps;
delete ps;
return;
}
- 스마트 포인터 (smart pointer)
- 포인터처럼 행동하는 클래스 객체이다.
new
를 통해 얻어지는 주소를 대입할 포인터를 정의한다.- 그리고 이 객체의 수명이 다 하면, 파괴자는
delete
를 사용해서 메모리를 자동으로 해제한다. (p.1230 그림 16.2 참고) auto_ptr
,unique_ptr
,shared_ptr
,weak_ptr
memory
헤더파일에 정의되어있다.
// auto_ptr는 클래스 템플릿을 사용해서 포인터의 종류를 구체화한다.
template<class X>
class auto_ptr
{
public:
explicit auto_ptr(X* p = 0) throw(); // 생성자
};
int main()
{
auto_ptr<int> ptr(new int); // int형을 가리키는 auto_ptr이다.
auto_ptr<string> ptr(new string); // string형을 가리키는 auto_ptr이다.
}
auto_ptr
로 이전 예제의 문제점을 수정해보자.
#include <memory>
void Remodel(string& str)
{
//string* ps = new string(str);
auto_ptr<string> ps(new string(str)); // auto_ptr을 사용한다.
//...
if (weirdThing())
throw exception(); // auto_ptr이 동적 메모리도 알아서 해제해준다.
str = *ps;
//delete ps; 이것은 이제 필요없다.
return;
}
- 스마트 포인터 생성자에
explicit
이 있으므로 명시적 변환만 허용된다.
shared_ptr<double> ds;
double* d = new double;
ds = d; // (X) 암시적 변환은 안 된다.
ds = shared_ptr<double>(d); // (O) 명시적 변환은 된다.
- 스마트 포인터를 사용하면 안 되는 경우
delete
하려는 메모리가 힙에 없는 경우
string str("I want to study.");
shared_ptr<string> pstr(&str); // (X)
- 문제 상황
- 아래 예제는 한 번 해제한 메모리를 두 번 해제하게 된다.
- 각각의 스마트 포인터는 이 상황을 어떻게 해결할까?
auto_ptr<string> ps(new string("I'm happy."));
auto_ptr<string> pps;
pps = ps;
// 이렇게 하면 ps와 pps 모두 하나의 똑같은 메모리를 가리키게 된다.
// 일반 포인터면 괜찮겠지만, 스마트 포인터는 delete 자동으로 호출하니까,
// 그럼 delete도 두 번 될텐데.. 각각의 스마트 포인터는 이 상황을 어떻게 해결했을까?
- 해결 방안
- (1) 깊은 복사를 하는 대입연산자를 정의한다.
- (2) 소유권 개념을 도입한다.
auto_ptr
과shared_ptr
의 전략이다.- 스마트 포인터가 그 객체를 소유하는 경우에만 파괴자가 그 객체를 삭제한다. 그러고 나서, 대입을 통해 소유권을 이전시킨다.
- (3) 참조 카운팅(reference counting)
shared_ptr
의 전략이다.- 특정 객체를 참조하는 스마트 포인터들이 몇 개인지 추적한다.
- 대입할 때마다 참조 카운팅이 1씩 증가한다.
- 스마트 포인터의 수명이 다하면 1씩 감소한다.
- 그리고 마지막으로 스마트 포인터의 수명이 다했을 때
delete
가 호출된다.
- 해결 방안 비교
// auto_ptr
auto_ptr<string> ps(new string("I'm happy."));
auto_ptr<string> pps;
pps = ps; // ps의 소유권이 pss에게 넘어가서 ps는 더 이상 그 문자열을 참조하지 않는다.
cout << *ps << endl; // (X) 실행 후 프로그램 크래시가 발생한다.
// shared_ptr
shared_ptr<string> ps(new string("I'm happy."));
shared_ptr<string> pps;
pps = ps; // 참조가 2번이 된다.
cout << *ps << endl; // (O)
// 참조카운트를 2에서 1로(pps), 1에서 0으로 줄이고(ps), 0이 되었을 때 힌 번 메모리를 해제한다.
// unique_ptr
unique_ptr<string> ps(new string("I'm happy."));
unique_ptr<string> pps;
pps = ps; // (X) 애초에 컴파일이 되지 않아서 실행도 안 된다.
cout << *ps << endl;
unique_ptr
이auto_ptr
보다 좋은 점- 프로그램 크래시 보다 컴파일 에러가 더 안정적이다.
- 대입 시 원본 객체가 임시 rvalue라면 그것을 허용한다.
- 배열을 가리킬 수 있다.
auto_ptr
은new
와delete
는 사용할 수 있지만new []
,delete []
은 안 된다. 반면,unique_ptr
은 둘 다 가능하다.
unique_ptr<string> p1(new string("Uniquely special"));
unique_ptr<string> p2;
p2 = p1; // (X) 컴파일 에러
p2 = unique_ptr<string>(new string("Uniquely special"));
// (O) 임시 rvalue이므로, 임시 객체는 삭제된다. 따라서 p2가 유일하기 때문에 이것은 허용 된다.
// unique_ptr을 위한 대입 방법을 이렇게 제안한다.
unique_ptr<string> Demo(const char* s)
{
unique_ptr<string> temp(new string(s));
return temp;
}
int main()
{
unique_ptr<string> ps;
ps = Demo("Uniquely Special"); // (O)
}
string strs[3]{ "AAA", "BBB", "CCC"};
auto_ptr<string[]> psa(strs); // (X)
unique_ptr<string[]> psu(strs); // (O) 배열을 가리킬 수 있다.
unique_ptr
의 대입을 가능하게 해주는move()
표준 라이브러리 함수
unique_ptr<string> p1(new string("One"));
unique_ptr<string> p2 = move(p1);
p1 = unique_ptr<string>(new string("And more"));
- 스마트 포인터를 선택하는 방법
- 하나의 객체를 여러개의 포인터가 가리켜야 한다
shared_ptr
- 다중 포인터가 필요 없다.
unique_ptr
new
로 대입된 메모리를 리턴하는 함수의 리턴 타입?unique_ptr
- 하나의 객체를 여러개의 포인터가 가리켜야 한다
- 만약 하나의
unique_ptr
에서 다른unique_ptr
로 대입이 가능한 경우라면shared_ptr
에unique_ptr
을 대입할 수 있다.
shared_ptr<string> ps;
ps = unique_ptr<string>(new string("Uniquely special")); // (O)
ps = Demo("Uniquely Special"); // (O)