다음 코드를 실행시켜보면 "abc"가 잘 출력이 될까?

CString s = L"abc";
wprintf(L"%s", s);

당연히 잘 출력된다.
대부분의 사람들은 이 부분을 원래 그런거 아니었어? 하고 대수롭지 않게 넘어간다.

어잉, 그런가?

그렇다면 다음 코드는 어떨까? 이번엔 CString이 아니라 std::wstring을 사용했다.

std::wstring s = L"abc";
wprintf(L"%s", s); // this is wrong

컴파일은 잘 되지만 이번엔 제대로 출력되지 않는다.
형식 문자열에 포인터를 넣지 않고 wstring 객체 자체를 쑤셔 넣었기 때문이다.
사람들이 흔하게 하는 실수이며, 경험있는 프로그래머들이 형식 문자열을 사용하지 말라고 하는 이유이다.

제대로 출력하려면

wprintf(L"%s", s.c_str());

이렇게 써주어야 한다.

이상하다?
CString은 그냥 넣어도 잘 되고 std::wstring은 .c_str() 함수를 호출해야만 되는걸까.
CString에 뭔가 비밀이 있는건가?

있다.

조금 더 주의 깊은 사람들은 CString의 구현을 살펴보고 LPCWSTR에 대한 형변환 연산자가 정의되어 있는 것을 발견해낸다.
아, 형변환 연산자가 있기 때문에 자동으로 형 변환이 되는거구나.

하지만 이는 형변환 연산자 때문이 아니다.

가변 인자 함수로 들어갈 때는 형변환이 발생하지 않는다.
함수 파라메터가 ‘가변’이고 무슨 타입이 들어올지도 적혀 있지 않는데 어떤 타입으로 형변환을 하는가?

CString의 코드가 잘 동작할 수 있는 비밀은 CString의 멤버 변수가 실제 문자열 데이터를 가리키고 있는 포인터 m_pszData 하나 밖에 없기 때문이다.
가상 함수도 없다. 객체의 맨 앞에 포인터를 가지고 있다는 것이 중요하다.

그림으로 그려보면 이렇게 생겼다.
CString의 내부
CString 의 내부

재밌지 않은가?
문자열 클래스가 문자열 길이를 위한 변수도 가지고 있지 않고 달랑 WCHAR* 하나만 가지고 있다니.

하지만 CString의 은밀한 곳에서는 메모리를 조금 더 할당해서 문자열 길이나 레퍼런스 카운트를 저장하고 CString에게는 pszData 하나만 노출시켜 놓는다.
그리고 이렇게 설계한 이유는 사람들이 저런 실수를 하더라도 잘 동작하도록 만들고 싶었기 때문일 것이다.

하지만 잘 동작한다고 해서 그렇게 쓰는 것이 옳다는 것은 아니다.
가변인자의 파라메터로는 POD 만 들어갈 수 있다.
NON-POD 타입이 들어갈 경우에는 모두 undefined behavior이다.
나중에는 컴파일러의 구현 변경에 따라 동작하지 않게 될 수도 있다는 뜻이다.
-말은 이렇게 해도 그런 일은 좀처럼 발생하지 않는다.

어쨌거나,

wprintf(L"%s", (LPCWSTR)s); 또는
wprintf(L"%s", s.GetString());

이라고 써주어야 올바른 C++ 문장이다.

함께 읽으면 좋은 글: