C++ string 다루기 1

3 분 소요

std::string

프로그램을 작성하다 보면 스트링을 사용할 일이 생기기 마련이다. C 언어에서는 단순히 null로 끝나는 문자 배열로 스트링을 표현했다. 하지만 일허게 하면 buffer overflow를 비롯한 다양한 문제 때문에 보안 취약점이 드러날 수 있다. C++ 표준 라이브러리는 이러한 문제를 방지하기 위해 std::string 클래스를 제공한다.

동적 스트링

스트링을 주요 객체로 제공하는 프로그래밍 언어를 보면 대체로 스트링의 크기를 임의로 확장하거나, 서브스트링(substring)을 추출하거나 교체하는 것처럼 고급 기능을 제공한다. 반면 C와 같은 언어는 스트링을 부가 기능처럼 취급한다. 그래서 스트링을 언어의 정식 데이터 타입으로 제ㅔ공하지 않고 단순히 고정된 크기의 바이트 배열로 처리했다. 반면 C++는 스트링을 핵심 테이터 타입으로 제공한다.

C 스타일

C 언어는 스트링을 문자 배열로 표현했다. 스트링의 마지막에 널 문자(\0)를 붙여서 스트링이 끝났음을 표현한다. 이러한 널 문자에 대한 공식 기호는 NUL이다. 여기서는 L이 두 개가 아니라 하냐며 NULL 포인터와는 다른 값아다. C++에서 제공하는 스트링이 훨씬 뛰어나지만 C언어에서 스트링을 다루는 방법도 알아둘 필요가 있다. 아직도 C 스타일 스트링을 쓰는 C++ 프로그램이 많기 때문이다. 대표적인 예로 인터페이스를 C스타일로 제공하는 외부 라이브러리나 운영체제와 연동하는 C++코드를 들 수 있다.
C 스트링을 다룰 때 \0 문자를 담을 공간을 깜빡하고 할당하지 않는 실수를 저지르기 쉽다. 예를 들어 'hello'란 스트링을 구성하는 문자는 다섯 개이지만, 메모리에 저장할 때는 문자 여섯개만큼의 공간이 필요하다. 'h', 'e', 'l', 'l', 'o', '\0'
C++는 C 언어에서 사용하던 스트링 연산에 대한 함수도 제공한다. 이러한 함수는 <cstring> 헤더 파일에 정의되어 있다. 이런 함수는 대체로 메모리 할당 기능을 제공하지 않는다. 예를 들어 strcpy() 함수는 스트링 타입 매개변수를 두 개 받아서 두번째 스트링을 첫 번째 스트링에 복사한다. 이때 두 스트링의 길이가 같은지 확인하지 않는다. 아래 코드를 확인해보자
char* copyString(const char* str)
{
    char * result = new char[strlen(str)];
    strcpy(result, str);
    return result;
}
위 코드는 오류가 하나 있다. strlen() 함수에서 리턴하는 값은 스트링을 저장하는 데 사용된 메모리 크기가 아니라 스트링 길이라는 점이다. 따라서 strlen()은 'hello'란 스트링에 대해 6이 아닌 5를 리턴한다. 따라서 스트링을 저장하는 데 필요한 메모리를 제대로 할당하려면 문자 수에 1을 더한 크기로 지정해야 한다. 항상 +1이 붙어서 지저분하지만 어쩔 수 없다. C 스타일의 스트링을 다룰 때는 항상 이 점을 명심해야 한다. 위 함수를 제대로 작성하면 다음과 같다.
char* copyString(const char* str)
{
    char* result = newe char[strlen(str) + 1];
    strcpy(result, str);
    return result;xs
}

스트링 리터럴

C++프로그램에서 스트링을 인용부호로 묶은 것을 종종 본 적이 있다. 예를들어 다음 코드는 hello란 스트링을 변수에 담지 않고 스트링값을 곧바로 화면에 출력한다.
std::cout << "hello" << std::endl;
여기 나온 'hello'처럼 변수에 담지 않고 곧바로 값으로 표현한 스트링을 스트링 리터럴(string literal)이라 부른다. 스트링 리터럴은 내부적으로 메모리의 읽기 전용 영역에 저장된다. 컴파일러는 같은 스트링 리터럴이 코드에 여러 번 나오면 그중 한 스트링에 대한 레퍼런스를 재사용 하는 방식으로 메모리를 절약한다. 수백개의 같은 리터럴을 코드에서 사용한다 해도 컴파일러는 hello에 대한 메모리 공간을 딱 하나만 할당한다. 이를 리터럴 풀링(literal pooling)이라 한다.

스트링 리터럴을 변수에 대입할 수는 있지만, 메모리의 읽기 전용 영역에 있게 되거나 동일한 리터럴을 여러 곳에서 공유할 수 있기 때문에 변수에 저장하면 위험하다. C++ 표준에서는 스트링 리터럴을 'const char가 n개인 배열'타입으로 정의하고 있다. 하지만 const가 없던 시절에 작성된 레거시 코드의 하위 호환성을 보장하도록 스트링 리터럴을 const char*가 아닌 타입으로 저장하는 컴파일러도 많다. const 없이 char* 타입 변수에 스트링 리터럴을 대입하더라도 그 값을 변경하지 않는 한 프로그램 실행에는 아무런 문제없다. 스트링 리터럴을 수정하는 동작에 대해서는 명확히 정의돼 있지 않다.

예를들어 다음과 같이 코드를 작성하면 결과를 예측할 수 없다.
char * ptr = "hello"; // 변수에 스트링 리터럴을 대입한다.
ptr[1] = 'a';         // 결과를 예측할 수 없다.

스트링 리터럴을 참조할 때는 const 문자에 대한 포인터를 사용하는 것이 더욱 안전하다.
const char* ptr = "hello";
ptr[1] = 'a';         // 읽기 전용 메모리(const로 선언했기 때문에)에 값을 쓰기 때문에 에러가 발생한다.

아래와 같이 초깃값을 설정할 수 있다.
char arr[] = "hello"; // 컴파일러는 적잘한 크기의 문자 배열 arr을 생성한다.
arr[1] = 'a';         // 여기서는 스트링을 수정할 수 있다.
위에서 컴파일러는 주어진 스트링을 충분히 담을 정도로 큰 배열을 생성한 뒤 여기에 실제 스트링값을 복사한다. 컴파일러는 위 arr 배열을 읽기 전용 메모리에 넣지 않으며 재사용하지도 않는다.

참고

  • 전문가를 위한 C++