MFC에서 CString을 char*로 변환하는 안전한 방법 총정리 (GetBuffer·strcpy·유니코드 함정까지)
TL;DR — MFC의 CString을 C 스타일 char* 문자열로 변환할 때 자주 겪는 메모리 누수와 유니코드 빌드 오류를 단계별로 짚고, 실무에서 바로 쓰는 안전한 변환 코드를 정리합니다.
들어가며: 왜 이 변환이 자꾸 발목을 잡을까
MFC로 윈도우 애플리케이션을 만들다 보면 CString은 거의 모든 곳에서 마주치는 문자열 타입입니다. 편집 컨트롤의 텍스트를 읽거나, 파일 경로를 다루거나, 로그를 찍을 때 자연스럽게 손이 갑니다. 그런데 정작 표준 C 라이브러리 함수(fopen, printf, 서드파티 SDK의 C API 등)에 이 값을 넘기려고 하면 갑자기 막힙니다. 그 함수들은 CString이 아니라 char* 혹은 const char*를 요구하기 때문입니다.
문제는 변환 자체가 어렵다기보다, 잘못 변환하면 컴파일은 되는데 런타임에 메모리가 깨지거나, 유니코드 빌드에서 엉뚱한 타입 오류가 나는 것입니다. 이 글에서는 CString을 char*로 안전하게 바꾸는 정석 방법을 단계별로 정리하고, 실무에서 반드시 알아야 할 함정들까지 짚어보겠습니다.
핵심 개념: CString은 그냥 char 배열이 아니다
먼저 오해를 하나 풀어야 합니다. CString은 단순한 문자 배열이 아니라 참조 카운트 기반으로 내부 버퍼를 관리하는 클래스입니다. 내부적으로 길이, 할당 크기, 실제 데이터 포인터를 따로 들고 있고, 복사 시에는 Copy-on-Write 전략을 씁니다.
또 하나 중요한 점은 빌드 설정에 따라 CString이 담는 문자 타입이 달라진다는 것입니다.
- 멀티바이트(MBCS) 빌드:
CString은CStringA와 같고, 내부는char기반 - 유니코드(Unicode) 빌드:
CString은CStringW와 같고, 내부는wchar_t(2바이트) 기반
대부분의 최신 Visual Studio 프로젝트는 기본이 유니코드입니다. 그래서 "예전 블로그 코드를 그대로 붙여넣었는데 타입 오류가 난다"는 상황이 자주 생깁니다. 이 점은 뒤에서 다시 다루겠습니다.
단계별 변환 절차
변환의 본질은 "CString이 들고 있는 문자 데이터를, 내가 직접 할당한 char 버퍼로 복사"하는 것입니다. 네 단계로 나뉩니다.
- 길이 측정 —
GetLength()로 실제 문자 수를 구합니다. - 버퍼 할당 — 널 종료 문자(
'\0')까지 고려해길이 + 1크기로 메모리를 확보합니다. - 복사 —
strcpy계열 함수로 내부 버퍼 내용을 옮깁니다. - 해제 — 동적으로 잡은 메모리는 사용이 끝나면 반드시
delete[]로 돌려줍니다.
정석 코드 예제 (MBCS 빌드 기준)
가장 기본적인 형태를 먼저 보겠습니다. 변수명과 주석은 실무 흐름에 맞게 새로 구성했습니다.
// 멀티바이트(MBCS) 빌드를 가정한 기본 변환
CString source = _T("로그 메시지 예시");
int charCount = source.GetLength(); // 문자열 길이 (널 문자 제외)
char* buffer = new char[charCount + 1]; // ★ +1 : 널 종료 공간 확보
// GetBuffer(0)은 내부 버퍼의 시작 주소를 돌려준다
strcpy(buffer, source.GetBuffer(0));
source.ReleaseBuffer(); // GetBuffer를 썼으면 짝으로 풀어준다
// ... buffer를 C API에 전달해서 사용 ...
delete[] buffer; // 동적 메모리 반환
buffer = nullptr; // 댕글링 포인터 방지
원본의 흔한 예제는 new char[length]처럼 길이만큼만 할당하는데, 이것이 첫 번째 버그 포인트입니다. strcpy는 널 문자까지 복사하므로 한 바이트가 모자라 버퍼 오버런이 발생할 수 있습니다. 반드시 +1을 해야 합니다.
더 안전한 strcpy_s 사용
요즘 컴파일러는 strcpy를 보안 경고(C4996)로 막습니다. 대상 버퍼 크기를 함께 넘기는 strcpy_s를 쓰는 편이 안전합니다.
CString source = _T("안전 복사 예시");
int bufSize = source.GetLength() + 1;
char* buffer = new char[bufSize];
// 대상 크기를 명시해 오버런을 컴파일러/런타임이 검증
strcpy_s(buffer, bufSize, CT2A(source)); // CT2A: 변환 매크로 (아래 설명)
delete[] buffer;
buffer = nullptr;
유니코드 빌드라면? 변환 매크로가 필요하다
여기가 실무에서 가장 많이 막히는 지점입니다. 유니코드 빌드에서는 CString의 내부가 wchar_t이기 때문에, GetBuffer(0)가 돌려주는 것은 char*가 아니라 wchar_t*입니다. 그래서 strcpy(char*, wchar_t*)는 타입이 맞지 않아 컴파일 오류가 납니다.
이때는 ATL/MFC가 제공하는 문자열 변환 매크로를 쓰는 것이 가장 깔끔합니다.
#include <atlconv.h> // 변환 매크로 헤더
CString wide = _T("유니코드에서 char로");
// CT2A : 현재 문자셋(T) → ANSI(char) 변환
// 임시 객체가 변환된 char* 버퍼를 들고 있다
CT2A converted(wide);
const char* ansiStr = converted; // 바로 const char*로 사용 가능
printf("%s\n", ansiStr);
매크로 이름은 C + 원본타입 + 2(to) + 대상타입 규칙을 따릅니다.
| 매크로 | 변환 방향 |
|---|---|
CT2A |
TCHAR → char (ANSI) |
CT2W |
TCHAR → wchar_t |
CW2A |
wchar_t → char |
CA2W |
char → wchar_t |
CT2A는 빌드 설정에 따라 자동으로 적절히 동작하므로, MBCS/유니코드 양쪽을 모두 지원해야 하는 코드에서 특히 유용합니다.
직접 할당이 필요 없을 때: 더 간단한 길
C API에 읽기 전용으로 잠깐 넘기기만 하면 되는 경우라면, 굳이 new로 메모리를 잡지 않아도 됩니다. MBCS 빌드에서는 CString을 const char*로 암시적 변환할 수 있고, 유니코드라면 CStringA로 한 번 받아주면 됩니다.
// 읽기 전용으로 잠깐 쓸 때 — 동적 할당 불필요
CString src = _T("임시 전달용");
CStringA narrow(src); // 유니코드 → ANSI 자동 변환
SomeCApiFunction(narrow); // const char* 인자에 그대로 전달
// narrow의 수명이 끝나면 자동 정리 — delete 불필요
이 방식은 메모리 관리 부담이 없어 버그 가능성이 훨씬 낮습니다. 굳이 char 버퍼를 소유해야 하는 경우(나중에 내용을 수정하거나, 함수 스코프 밖으로 넘겨야 하는 경우)에만 new/delete 방식을 쓰세요.
흔한 실수와 엣지케이스
실무에서 반복적으로 터지는 문제들을 정리합니다.
- 널 종료 공간 누락:
new char[length]는 거의 항상 틀립니다.length + 1이 정답입니다. - GetBuffer 후 ReleaseBuffer 누락:
GetBuffer()는 CString에게 "내가 내부 버퍼를 직접 만질 테니 길이 관리를 잠시 멈춰라"라고 알리는 함수입니다. 사용이 끝나면ReleaseBuffer()로 짝을 맞춰야 내부 상태가 일관됩니다. 단순 읽기만 한다면GetString()(읽기 전용const포인터 반환)이 더 안전합니다. - 반환된 포인터의 수명 착각:
(LPCTSTR)str또는 변환 매크로가 돌려준 포인터는 원본 CString이나 임시 객체가 살아있는 동안에만 유효합니다. 이 포인터를 멤버 변수에 저장해 두고 나중에 쓰면 이미 해제된 메모리를 가리킬 수 있습니다. - 멀티바이트 한글 처리: ANSI(
char) 환경에서 한글 한 글자는 2바이트입니다.GetLength()가 돌려주는 "문자 수"와 실제 "바이트 수"가 다를 수 있으므로, 바이트 단위 계산이 필요할 때는 주의해야 합니다. 유니코드 빌드를 쓰는 것이 근본 해법입니다. - delete vs delete[] 혼동: 배열로 할당(
new char[])했으면 반드시delete[]로 해제해야 합니다.delete만 쓰면 미정의 동작입니다.
요약
| 상황 | 권장 방법 |
|---|---|
| 읽기 전용으로 잠깐 전달 | CStringA 또는 (LPCSTR) 암시적 변환 |
| char 버퍼를 소유·수정해야 함 | new char[len+1] + strcpy_s + delete[] |
| 유니코드 빌드에서 char 필요 | CT2A / CW2A 변환 매크로 |
| 단순 길이/내용 확인 | GetString() (읽기 전용) |
핵심은 세 가지입니다. (1) 항상 널 종료 공간 +1을 잡을 것, (2) 동적 할당했으면 반드시 짝맞춰 해제할 것, (3) 유니코드 빌드에서는 변환 매크로를 쓸 것. 이 세 가지만 지켜도 CString 변환에서 생기는 메모리 사고의 대부분을 막을 수 있습니다.
AI에게 물어볼 때 (프롬프트 팁)
이런 레거시 MFC 문제는 검색보다 AI에게 맥락을 충분히 주고 물어보는 편이 빠를 때가 많습니다. ChatGPT나 Claude에게 질문할 때는 빌드 설정·에러 메시지·목적을 함께 주는 것이 핵심입니다. Prompt Architect에서 권장하는 구체적 프롬프트 예시입니다.
에러 진단형: "Visual Studio 2022 유니코드 빌드 MFC 프로젝트입니다.
CString을char*로 변환해 C 라이브러리 함수에 넘기려는데strcpy(char*, wchar_t*)타입 오류가 납니다. 변환 매크로를 사용한 최소 수정 코드와, 그 포인터의 수명 주의점을 함께 알려주세요."코드 리뷰형: "아래 MFC 코드에서 메모리 누수나 버퍼 오버런 가능성을 모두 짚고, 안전한 버전으로 리팩터링해 주세요. 멀티바이트/유니코드 양쪽 빌드에서 모두 동작해야 합니다. [코드 붙여넣기]"
개념 비교형: "MFC에서 CString을 char*로 바꾸는 방법 3가지(GetBuffer+strcpy, CStringA 변환, CT2A 매크로)의 장단점을 표로 정리하고, 각각 언제 쓰면 좋은지 실무 기준으로 설명해 주세요."
질문에 **환경(빌드 모드)·증상(정확한 에러 문구)·의도(읽기 전용인지 소유인지)**를 함께 넣을수록 AI가 정확한 답을 줍니다. 좋은 답은 좋은 질문에서 나옵니다.