1. C 스타일 스트링
C 언어는 스트링을 문자 배열로 표현했다.
스트링의 마지막에 널 문자(\0)를 붙여서 스트링이 끝났음을 표현했다.
이러한 널 문자에 대한 공식 기호는 NUL이다. 여기서는 L이 두 개가 아니라 하나며 NULL 포인터와는 다른 값이다.
char* copyString(const char* str)
{
char* result = new char[strlen(str)]; // 버그! 한 칸 부족하다.
strcpy(result, str);
return result;
}
위 copyString() 함수 코드에 오류가 하나 있다.
strlen() 함수에서 리턴하는 값은 스트링을 저장하는 데 사용된 메모리 크기가 아니라 스트링 길이라는 점이다.
따라서 strlen()은 'hello'란 스트링에 대해 6이 아닌 5를 리턴한다.
따라서 스트링을 저장하는 데 필요한 메모리를 제대로 할당하려면 문자 수에 1을 더한 크기로 지정해야 한다.
항상 +1이 붙어서 지저분하지만 어쩔 수 없다.
C 스타일의 스트링을 다룰 때는 항상 이 점을 명심해야 한다.
위 함수를 제대로 작성하면 다음과 같다.
char* copyString(const char* str)
{
char* result = new char[strlen(str) + 1];
strcpy(result, str);
return result;
}
C와 C++에서 제공하는 sizeof() 연산자는 데이터 타입이나 변수의 크기를 구하는 데 사용된다.
예를 들어 sizeof(char)는 1을 리턴하는데, char의 크기가 1바이트이기 때문이다.
하지만 C 스타일 스트링에 적용할 때는 sizeof()와 strlen()의 결과가 전혀 다르다.
따라서 스트링의 길이를 구할 때는 절대로 sizeof()를 사용하면 안 된다.
sizeof()의 리턴 값은 C 스타일 스트링에 저장된 방식에 따라 다르기 때문이다.
예를 들어 다음과 같이 스트링을 char[]로 저장하면 sizeof()는 '\0'을 포함하여 그 스트링에 대해 실제로 할당된 메모리의 크기를 리턴한다.
char text1[] = "abcdef";
size_t s1 = sizeof(text1); // 7
size_t s2 = strlen(text1); // 6
반면 C 스타일 스트링을 char*로 저장했다면 sizeof()는 포인터의 크기를 리턴한다.
const char* text2 = "abcdef";
size_t s3 = sizeof(text2); // 플랫폼마다 다르다.
size_t s4 = strlen(text2); // 6
이 코드를 32비트 모드에서 컴파일하면 s3의 값은 4고, 64비트 모드에서 컴파일하면 8이다.
sizeof()가 포인터 타입인 const char*의 크기를 리턴하기 때문이다.
2. 스트링 리터럴
c++ 프로그램에서 스트링을 인용부호로 묶은 것을 본 적이 있을 것이다.
예를 들어 다음 코드는 hello란 스트링을 변수에 담지 않고 스트링값을 곧바로 화면에 출력한다.
cout << "hello" << endl;
여기 나온 'hello'처럼 변수에 담지 않고 곧바로 값으로 표현한 스트링을 스트링 리터럴이라 부른다.
스트링 리터럴은 내부적으로 메모리의 읽기 전용 영역에 저장된다.
그래서 컴파일러는 같은 스트링 리터럴이 코드에 여러 번 나오면 그중 한 스트링에 대한 레퍼런스를 재사용하는 방식으로 메모리를 절약한다.
다시 말해 코드에서 'hello'란 스트링 리터럴을 500번 넘게 작성해도 컴파일러는 hello에 대한 메모리 공간을 딱 하나만 할당한다. 이를 리터럴 풀링이라 한다.
스트링 리터럴을 변수에 대입할 수는 있지만, 메모리의 읽기 전용 영역에 있게 되거나 동일한 리터럴을 여러 곳에서 공유할 수 있기 때문에 변수에 저장하면 위험하다.
c++ 표준에서는 스트링 리터럴을 'const char가 n개인 배열' 타입으로 정의하고 있다.
하지만 const가 없던 시절에 작성된 레거시 코드의 하위 호환성을 보장하도록 스트링 리터럴을 const char*가 아닌 타입으로 저장하는 컴파일러도 많다.
const 없이 char* 타입 변수에 스트링 리터럴을 대입하더라도 그 값을 변경하지 않는 한 프로그램 실행에는 아무런 문제가 없다.
스트링 리터럴을 수정하는 동작에 대해서는 명확히 정의되어 있지 않다.
따라서 프로그램이 갑자기 죽을 수도 있고, 실행은 되지만 겉으로 드러나지 않는 효과가 발생할 수도 있고, 수정 작업을 그냥 무시할 수도 있고, 의도한 대로 동작할 수도 있다.
구체적인 동작은 컴파일러마다 다르다.
예를 들어 다음과 같이 코드를 작성하면 결과를 예측할 수 없다.
char* ptr = "hello"; // 변수에 스트링 리터럴을 대입한다.
ptr[1] = 'a'; // 결과를 예측할 수 없다.
스트링 리터럴을 참조할 때는 const 문자에 대한 포인터를 사용하는 것이 훨씬 안전하다.
다음 코드도 위와 똑같은 버그를 담고 있지만 스트링 리터럴을 const char* 타입 변수에 대입했기 때문에 컴파일러는 읽기 전용 메모리에 쓰기 작업을 실행하는 것을 걸러낼 수 있다.
const char* ptr = "hello"; // 변수에 스트링 리터럴을 대입한다.
ptr[1] = 'a'; // 읽기 전용 메모리에 값을 쓰기 때문에 에러가 발생한다.
문자 배열(char[])의 초깃값을 설정할 때도 스트링 리터럴을 사용한다.
이때 컴파일러는 주어진 스트링을 충분히 담을 정도로 큰 배열을 생성한 뒤 여기에 실제 스트링값을 복사한다.
컴파일러는 이렇게 만든 스트링 리터럴을 읽기 전용 메모리에 넣지 않으며 재사용하지도 않는다.
char arr[] = "hello"; // 컴파일러는 적절한 크기의 문자 배열 arr을 생성한다.
arr[1] = 'a'; // 이제 스트링을 수정할 수 있다.
3. 로 스트링 리터럴
로 스트링 리터럴(raw string literal)이란 여러 줄에 걸쳐 작성한 스트링 리터럴로서, 그 안에 담긴 인용 부호를 이스케이프 시퀀스로 표현할 필요가 없고, \t나 \n같은 이스케이프 시퀀스를 일반 텍스트로 취급한다.
예를 들어 일반 스트링 리터럴을 다음과 같이 작성하면 스트링 안에 있는 큰따옴표를 이스케이프 시퀀스로 표현하지 않았기 때문에 컴파일 에러가 발생한다.
const char* str = "Hello "World"!"; // 에러가 발생한다!
이럴 때는 큰따옴표를 다음과 같이 이스케이프 시퀀스로 표현한다.
const char* str = "Hello \"World!\"!";
하지만 로 스트링 리터럴을 사용하면 인용부호를 이스케이프 시퀀스로 표현하지 않아도 된다.
로 스트링 리터럴은 R"(로 시작해서) "로 끝난다.
const char* str = R"(Hello "World"!)";
로 스트링 리터럴을 사용하지 않고 여러 줄에 걸친 스트링을 표현하려면 스트링 안에서 줄이 바뀌는 지점에 \n을 넣어야 한다. 예를 들면 다음과 같다.
const char* str = "Line 1\nLine 2";
이 스트링을 콘솔에 출력하면 다음과 같이 나온다.
Line 1
Line 2
로 스트링 리터럴로 표현할 때는 다음과 같이 소스 코드에서 줄바꿈을 할 지점에 \n이스케이프 시퀀스를 입력하지 말고 그냥 엔터키를 누르면 된다. 그러면 앞에서 \n을 지정했을 때와 똑같이 출력된다.
const char* str = R"(Line 1
Line2)";
로 스트링 리터럴은 )"로 끝나기 때문에 그 안에 )"를 넣을 수 없다.
예를 들어 다음과 같이 중간에 )"가 들어가면 에러가 발생한다.
const char* str = R"(Embedded )" characters)"; // 에러가 발생한다.
)" 문자를 추가하려면 다음과 같이 확장 로 스트링 리터럴(extended raw string literal) 구문으로 표현해야 한다.
R"d-char-sequence(r-char-sequence)d-char-sequence"
여기서 r-char-sequence에 해당하는 부분이 실제 로 스트링이다.
d-char-sequence라고 표현한 부분은 구분자 시퀀스로서, 반드시 로 스트링 리터럴의 시작과 끝에 똑같이 나와야 한다.
이 구분자 시퀀스는 최대 16개의 문자를 가질 수 있다. 이때 구분자 시퀀스는 로 스트링 리터럴 안에 나오지 않는 값으로 지정해야 한다. 앞에 나온 스트링에서 고유한 구분자 시퀀스를 사용하도록 수정하면 다음과 같다.
const char* str = R"-(Embedded )" characters)-";
로 스트링 리터럴을 사용하면 데이터베이스 쿼리 스트링이나 정규표현식, 파일 경로 등을 쉽게 표현할 수 있다.
참고
- 전문가를 위한 c++ 책
'C, C++ > 전문가를 위한 C++ (책)' 카테고리의 다른 글
전문가를 위한 C++ : 3장 코딩 스타일 (0) | 2022.06.11 |
---|---|
전문가를 위한 C++ : 2장 스트링 (2) (0) | 2022.06.09 |
전문가를 위한 C++ : 1장 (4) (0) | 2022.06.07 |
전문가를 위한 C++ : 1장 (3) (0) | 2022.06.06 |
전문가를 위한 C++ : 1장 (2) (0) | 2022.06.04 |