(전체가 아니라 C#과 차이가 있는 부분을 중심으로 요약 정리)

동적 스트링

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

C 스타일 스트링

C 언어는 스트링을 문자 배열로 표현했다. 스트링의 마지막에 널 문자(\0)를 붙여서 스트링이 끝났음을 표현했다. 이러한 널 문자에 대한 공식 기호는 NUL 이다. 여기서 L이 두 개가 아니며 NULL 포인터와는 다른 값이다.

(이하 C의 스트링 내용 생략. 스트링을 배열로 다루는데다가 마지막에 \0을 추가하기 위해 배열의 길이를 +1 더 써야 한다는데, C를 쓰는 사람이 아니라 요즘 프로그래밍 환경을 쓰는 사람이라면 굳이 살펴보지 않아도 될 듯)

Caution) 마이크로소프트 비주얼 스튜디오에서 C 스타일 스트링 함수를 사용하면 컴파일러에서 보안 관련 경고나 이 함수가 폐기됐다는 에러 메시지가 출력될 수 있다.

스트링 리터럴

cout << "hello" << endl;

위 예시와 같이 ‘hello’ 처럼 변수에 담지 않고 곧바로 값을 표현한 스트링을 스트링 리터럴이라 부른다. 스트링 리터럴은 내부적으로 읽기 전용 영역에 저장된다. 그래서 컴파일러는 스트링 리터럴이 코드에 여러 번 나오면 그중 한 스트링에 대한 레퍼런스를 재사용하는 방식으로 메모리를 절약한다.

다시 말해 코드에서 ‘hello’란 스트링 리터럴을 500번 넘게 사용해도 컴파일러는 hello에 대한 메모리 공간을 딱 하나만 할당하는데, 이를 리터럴 풀링(literal pooling)이라 한다.

스트링 리터럴을 변수에 대입할 수 있지만 메모리의 읽기 전용 영역에 있게 되거나 동일한 리터럴을 여러 곳에서 공유할 수 있기 때문에 변수에 저장하면 위험하다.

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'; // 이제 스트링을 수정할 수 있다.