2 분 소요

메신저를 만들어보고 있는데 서버는 C++와 MFC로, 클라이언트는 WPF와 C#으로 만들어보고 있다.

패킷을 주고 받을 때 유니코드로 처리해야 해서 wchar_t* 형태로 패킷을 보내게 만들었는데, 이러다 보니 CStringwchar_t*로 다뤄야 할 경우가 많았다.

2026년 수정 안내 원래 이 글에 적었던 A2W 방식에는 세 가지 문제가 있었다. (1) 유니코드 빌드에서 CString은 이미 wchar_t 기반이라 ANSI→Wide 변환인 A2W를 쓸 자리가 아니고, (2) A2W가 돌려주는 포인터는 스택에 할당되어 함수를 벗어나면 무효가 되며, (3) 예제 문자열의 역슬래시가 이스케이프되지 않았다. 전부 바로잡는다.

유니코드 빌드라면 변환이 필요 없다

Visual Studio의 기본 설정인 유니코드 문자 집합에서는 CString이 곧 CStringW이고 내부 버퍼가 이미 wchar_t다. 따라서 별도 변환 없이 그대로 쓰면 된다.

CString strFileName = _T( "J:\\RefPrograms\\WordCreationTest200\\REPORT_test.docx" );

// 읽기 전용으로 넘길 때 - 가장 흔한 경우
LPCWSTR szFileName = strFileName;          // 암시적 변환 연산자가 있다
SomeApiThatTakesConstWchar( szFileName );

경로 문자열의 \는 C++ 문자열 리터럴에서 이스케이프 문자이므로 \\로 써야 한다. 원래 글의 "J:\RefPrograms\..."\R, \W가 유효하지 않은 이스케이프 시퀀스라 경고가 나거나 의도와 다른 문자열이 된다. C++11부터는 원시 문자열 리터럴을 써도 된다.

CString strFileName = _T( R"(J:\RefPrograms\WordCreationTest200\REPORT_test.docx)" );

수정 가능한 버퍼가 필요하다면

API가 const가 아닌 wchar_t*를 요구할 때만 GetBuffer를 쓴다. 반드시 ReleaseBuffer와 짝을 맞춰야 한다.

const int kMaxPath = MAX_PATH;

wchar_t* szBuffer = strFileName.GetBuffer( kMaxPath );
SomeApiThatModifies( szBuffer, kMaxPath );
strFileName.ReleaseBuffer();               // 짝을 맞추지 않으면 내부 상태가 깨진다

GetBuffer 이후 ReleaseBuffer 전까지는 해당 CString 객체의 다른 멤버 함수를 호출하면 안 된다.

A2W를 쓰면 안 되는 이유

// 이렇게 하면 안 된다
USES_CONVERSION;
wchar_t* szFileName = A2W( strFileName.GetBuffer() );
  • A2WANSI 문자열을 Wide로 변환하는 매크로다. 유니코드 빌드에서 GetBuffer()가 돌려주는 것은 이미 wchar_t*라 타입이 맞지 않는다. 굳이 ANSI/유니코드 빌드 양쪽을 지원해야 한다면 A2W가 아니라 T2W(또는 CT2W)를 써야 한다.
  • 더 중요한 문제는 수명이다. A2W 계열 매크로는 _alloca호출한 함수의 스택 프레임에 버퍼를 잡는다. 그 포인터를 멤버 변수에 저장하거나 함수 밖으로 반환하면 즉시 댕글링 포인터가 된다. 변환 결과를 오래 들고 있어야 한다면 CStringW에 담아야 한다.

패킷에 담을 때

문자열을 네트워크로 보낼 때는 wchar_t*를 그대로 memcpy 하기보다 인코딩을 명시적으로 정하는 편이 안전하다. wchar_t의 크기가 윈도우는 2바이트, 리눅스/macOS는 4바이트로 다르기 때문에, C# 클라이언트나 다른 플랫폼과 붙일 때 문제가 된다.

UTF-8로 바꿔 보내고 받는 쪽에서 되돌리는 방식을 권한다.

// CStringW -> UTF-8
// -1을 넘기면 널 종료 문자까지 포함한 길이가 반환되므로, 실제 문자열 길이는 len - 1이다.
int len = ::WideCharToMultiByte( CP_UTF8, 0, strFileName, -1, nullptr, 0, nullptr, nullptr );
std::string utf8( len - 1, '\0' );   // 널 종료 문자는 std::string에 담지 않는다
::WideCharToMultiByte( CP_UTF8, 0, strFileName, -1, utf8.data(), len, nullptr, nullptr );

C# 쪽에서는 Encoding.UTF8로 그대로 받으면 된다. utf8.size()만큼만 네트워크로 보내면 널 종료 문자는 섞이지 않는다.

댓글 남기기