Skip to main content

[C++ 기초 플러스] Chapter 16. (1) string 클래스, 스마트 포인터 템플릿 클래스

C++ 기초 플러스 책을 읽고 공부한 노트입니다.




string 클래스 #

  • string 헤더파일을 통해 지원된다.
    • C 스타일은 string.hcstring이다.

  • 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객체를 생성한다 (디폴트 생성자)
  • (5) string(const char* s, size_type n)

    • string객체를 s가 자시하는 NBTS로 초기화하되, NBTS의 크기를 초과하더라도 n개의 문자까지 진행한다.
  • (6) template<class Iter> string(Iter begin, Iter end)

    • string객체를 beginend - 1 범위에 있는 [begin, end) 값들로 초기화한다. beginend는 포인터와 비슷한 역할을 하여 위치를 지정한다. 그 범위는 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로 초기화한다. strconst가 아니므로 바뀔 수 있다. (move 생성자)
    • 컴파일러는 경우에 따라 성과를 최적화하기 위해서 복사 생성자 대신 이동 생성자를 사용한다.
  • (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가 설정된다.

  • 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_ptrshared_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_ptrauto_ptr보다 좋은 점
    • 프로그램 크래시 보다 컴파일 에러가 더 안정적이다.
    • 대입 시 원본 객체가 임시 rvalue라면 그것을 허용한다.
    • 배열을 가리킬 수 있다.
      • auto_ptrnewdelete는 사용할 수 있지만 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_ptrunique_ptr을 대입할 수 있다.
shared_ptr<string> ps;
ps = unique_ptr<string>(new string("Uniquely special")); // (O)
ps = Demo("Uniquely Special"); // (O)