MFC CString 완전 정복: 문자열 처리부터 흔한 함정까지

Prompt Architect 편집팀 · 2026-06-18 · 7분

TL;DR — MFC 개발의 핵심인 CString 클래스를 선언·결합·검색·변환 단계로 정리하고, 유니코드와 메모리 관련 실수까지 실무 관점으로 다룹니다.

들어가며: 왜 CString인가

Windows 데스크톱 애플리케이션을 MFC로 개발하다 보면 가장 먼저, 그리고 가장 자주 마주치는 타입이 바로 CString입니다. C 스타일의 char*wchar_t*를 직접 다루다 보면 버퍼 크기 계산, 널 종료 문자(\0) 관리, 메모리 해제 같은 잡일에 시간을 다 빼앗기게 됩니다. 결합 한 번 하려고 strcpystrcat을 번갈아 호출하다가 버퍼 오버런으로 프로그램이 죽는 경험, MFC 개발자라면 한 번쯤은 있을 겁니다.

CString은 이런 저수준 문자열 관리의 부담을 통째로 추상화해 줍니다. 내부적으로 메모리를 동적으로 늘리고 줄이며, 참조 카운팅을 통해 복사 비용까지 최적화합니다. 이 글에서는 단순한 메서드 나열을 넘어, 실제 코드를 작성할 때 알아야 할 동작 원리와 자주 빠지는 함정을 단계별로 짚어 보겠습니다.

1. CString의 기본 성격

CString은 MFC와 ATL이 공유하는 CStringT 템플릿의 인스턴스입니다. 프로젝트의 문자 집합 설정에 따라 그 정체가 달라진다는 점이 핵심입니다.

  • 유니코드 빌드: CStringCStringW(와이드 문자, wchar_t 기반)가 됩니다.
  • 멀티바이트 빌드: CStringCStringA(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 직접 비교* CStringchar*==로 비교하면 의도와 다르게 동작할 수 있습니다. 항상 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가지 기준 점수를 확인해 보세요.