MFC CString 완전 정복: 문자열 처리부터 흔한 함정까지
TL;DR — MFC 개발의 핵심인 CString 클래스를 선언·결합·검색·변환 단계로 정리하고, 유니코드와 메모리 관련 실수까지 실무 관점으로 다룹니다.
들어가며: 왜 CString인가
Windows 데스크톱 애플리케이션을 MFC로 개발하다 보면 가장 먼저, 그리고 가장 자주 마주치는 타입이 바로 CString입니다. C 스타일의 char*나 wchar_t*를 직접 다루다 보면 버퍼 크기 계산, 널 종료 문자(\0) 관리, 메모리 해제 같은 잡일에 시간을 다 빼앗기게 됩니다. 결합 한 번 하려고 strcpy와 strcat을 번갈아 호출하다가 버퍼 오버런으로 프로그램이 죽는 경험, MFC 개발자라면 한 번쯤은 있을 겁니다.
CString은 이런 저수준 문자열 관리의 부담을 통째로 추상화해 줍니다. 내부적으로 메모리를 동적으로 늘리고 줄이며, 참조 카운팅을 통해 복사 비용까지 최적화합니다. 이 글에서는 단순한 메서드 나열을 넘어, 실제 코드를 작성할 때 알아야 할 동작 원리와 자주 빠지는 함정을 단계별로 짚어 보겠습니다.
1. CString의 기본 성격
CString은 MFC와 ATL이 공유하는 CStringT 템플릿의 인스턴스입니다. 프로젝트의 문자 집합 설정에 따라 그 정체가 달라진다는 점이 핵심입니다.
- 유니코드 빌드:
CString은CStringW(와이드 문자,wchar_t기반)가 됩니다. - 멀티바이트 빌드:
CString은CStringA(char기반)가 됩니다.
요즘 Visual Studio 프로젝트는 기본이 유니코드이므로, 문자열 리터럴을 쓸 때는 반드시 _T() 또는 L 매크로로 감싸는 습관을 들여야 합니다. 그래야 어떤 빌드 설정에서도 코드가 깨지지 않습니다.
2. 선언과 초기화
가장 기본적인 형태입니다. 변수명을 실무에서 쓸 법한 이름으로 바꿔 보겠습니다.
// _T() 매크로로 감싸야 유니코드/멀티바이트 양쪽에서 안전하다
CString userName = _T("홍길동");
CString greeting = _T("환영합니다");
CString empty; // 빈 문자열로 자동 초기화
// 동일 문자를 반복해 초기화하는 생성자
CString separator(_T('='), 30); // '=' 문자 30개로 구성된 구분선
CString은 생성과 동시에 빈 문자열로 안전하게 초기화되므로, 초기화하지 않은 포인터를 참조하는 사고가 원천 차단됩니다.
3. 문자열 결합과 형식화
문자열을 이어 붙이는 방법은 크게 두 가지입니다. 간단한 결합은 연산자로, 변수 값을 끼워 넣는 형식화는 Format()으로 처리합니다.
// 방법 1: + 연산자와 += 연산자
CString fullText = greeting + _T(", ") + userName + _T("님");
// 방법 2: Format()으로 형식화 (printf 스타일)
CString report;
report.Format(_T("%s, %s님 (방문 %d회)"), greeting, userName, 5);
여기서 한 가지 주의할 점이 있습니다. Format()의 %s 자리에 CString 객체를 그대로 넘기는 것은 사실 정의되지 않은 동작에 가깝지만, MFC의 CString은 이를 안전하게 처리하도록 설계되어 있어 관행적으로 허용됩니다. 다만 가독성과 이식성을 위해서는 (LPCTSTR)userName처럼 명시적으로 캐스팅하는 편이 더 안전합니다.
기존 문자열 뒤에 덧붙일 때는 AppendFormat()이 유용합니다.
CString log = _T("[로그] ");
log.AppendFormat(_T("처리 항목 수: %d"), 42);
4. 길이 측정과 부분 추출
CString path = _T("C:\\projects\\app\\main.cpp");
int len = path.GetLength(); // 문자 개수 반환(바이트 아님, 유니코드 주의)
// Left / Right / Mid 로 부분 문자열 추출
CString drive = path.Left(2); // "C:"
CString fileExt = path.Right(4); // ".cpp"
CString middle = path.Mid(3, 8); // 인덱스 3부터 8글자
GetLength()가 반환하는 값은 바이트 수가 아니라 문자(TCHAR) 개수입니다. 유니코드 빌드에서는 한 글자가 2바이트이므로, 메모리 크기를 계산할 때 혼동하지 않도록 주의해야 합니다.
5. 검색과 치환
특정 문자열의 위치를 찾거나 바꾸는 작업도 메서드 하나로 끝납니다.
CString sentence = _T("프롬프트 설계는 반복이 핵심이다");
int pos = sentence.Find(_T("반복")); // 찾으면 인덱스, 없으면 -1
if (pos != -1) {
// 발견 처리
}
// 부분 문자열 치환 (바뀐 개수를 반환)
int count = sentence.Replace(_T("반복"), _T("개선"));
// 앞뒤 공백 제거
CString raw = _T(" trimmed ");
raw.Trim(); // "trimmed"
Find()가 -1을 반환하는 경우를 반드시 분기 처리하는 것이 안전한 코드의 기본입니다.
6. 비교 연산
CString a = _T("apple");
CString b = _T("Apple");
// Compare: 대소문자 구분, 같으면 0
int r1 = a.Compare(b); // 0이 아님
// CompareNoCase: 대소문자 무시
int r2 = a.CompareNoCase(b); // 0 (같다고 판단)
// == 연산자도 사용 가능 (대소문자 구분)
if (a == _T("apple")) {
// 정확히 일치
}
Compare()는 0(같음), 음수(사전순 앞), 양수(사전순 뒤)를 반환하므로 정렬 로직에도 그대로 활용할 수 있습니다.
7. 다른 자료형과의 변환
숫자를 문자열로 바꿀 때는 Format()이 가장 직관적입니다. 반대로 문자열을 숫자로 바꿀 때는 표준 함수를 씁니다.
// 정수 -> 문자열
int score = 95;
CString scoreText;
scoreText.Format(_T("%d점"), score);
// 실수 -> 문자열 (소수점 둘째 자리)
double ratio = 0.8732;
CString ratioText;
ratioText.Format(_T("%.2f"), ratio);
// 문자열 -> 정수 / 실수
int n = _ttoi(scoreText); // TCHAR 대응 변환 함수
double d = _ttof(ratioText);
atoi 대신 _ttoi를, atof 대신 _ttof를 쓰는 이유는 유니코드/멀티바이트 양쪽을 모두 지원하기 위함입니다.
8. 흔한 실수와 엣지 케이스
(1) _T() 누락
유니코드 빌드에서 CString s = "hello";처럼 쓰면 컴파일 에러나 경고가 발생합니다. 반드시 _T("hello")로 감싸세요.
(2) GetBuffer 후 ReleaseBuffer 누락
C API에 직접 버퍼를 넘겨야 할 때 GetBuffer()를 호출했다면, 작업이 끝난 뒤 반드시 ReleaseBuffer()를 호출해야 합니다. 그렇지 않으면 내부 길이 정보가 갱신되지 않아 문자열이 잘리거나 메모리가 잠긴 상태가 됩니다.
CString buf;
TCHAR* p = buf.GetBuffer(256); // 최소 256자 버퍼 확보
::GetWindowText(hWnd, p, 256);
buf.ReleaseBuffer(); // 이걸 빼먹으면 길이 정보가 깨진다
(3) const char 직접 비교*
CString을 char*와 ==로 비교하면 의도와 다르게 동작할 수 있습니다. 항상 Compare() 또는 CString 간 비교를 사용하세요.
(4) 스레드 간 공유
CString은 참조 카운팅을 사용하므로, 여러 스레드가 같은 CString 인스턴스를 동시에 수정하면 경쟁 상태가 발생할 수 있습니다. 스레드 경계를 넘길 때는 복사본을 명시적으로 만드는 것이 안전합니다.
9. 요약
CString은 MFC 개발에서 문자열 처리의 부담을 크게 덜어 주는 클래스입니다. 핵심만 정리하면 다음과 같습니다.
- 리터럴은 항상
_T()로 감싼다. - 결합은
+/+=, 형식화는Format()/AppendFormat(). - 추출은
Left/Right/Mid, 검색은Find, 치환은Replace. - 비교는
Compare/CompareNoCase, 숫자 변환은Format과_ttoi/_ttof. GetBuffer뒤에는 반드시ReleaseBuffer.
이 원칙만 지켜도 문자열 관련 버그의 대부분을 예방할 수 있습니다.
10. AI에게 물어볼 때 (프롬프트 팁)
MFC처럼 레거시 문서가 흩어져 있는 기술은 AI에게 질문할 때 **맥락(빌드 설정, 컴파일러 버전)**을 함께 주는 것이 정확도를 크게 높입니다. Prompt Architect의 분석 기준으로 보면, 아래처럼 "역할 + 환경 + 제약 + 출력 형식"을 명시한 프롬프트가 좋은 점수를 받습니다.
예시 1 — 디버깅 상황
당신은 MFC 전문 C++ 개발자입니다. Visual Studio 2022, 유니코드 빌드 환경입니다.
CString::GetBuffer로 받은 포인터에 GetWindowText 결과를 넣었는데
문자열 길이가 0으로 나옵니다. 원인과 수정 코드를 한국어 주석과 함께 보여주세요.
예시 2 — 코드 변환
아래 char* 기반 문자열 처리 코드를 CString을 사용하도록 리팩터링해 주세요.
조건: (1) 모든 리터럴은 _T()로 감쌀 것 (2) Format 사용 (3) 변경 이유를 줄별로 설명.
[여기에 기존 코드 붙여넣기]
예시 3 — 비교 학습
CString, std::wstring, std::string의 차이를 메모리 관리, 유니코드 지원,
크로스플랫폼 이식성 세 축으로 표로 비교해 주세요. 각 항목마다 한 줄 코드 예시 포함.
이렇게 환경과 제약을 구체적으로 명시할수록 AI는 추측 대신 정확한 답을 내놓습니다. 프롬프트의 품질을 점검하고 싶다면 Prompt Architect 분석기로 8가지 기준 점수를 확인해 보세요.