[C++] C++에서 사용되는 문자열 타입(LPSTR, LPCSTR, LPCTSTR, LPCWSTR), 메모리 누수(memory leak) 체크하는 방법과 UTF8 변환하는 방법


Development note/C , C++ , MFC  2020. 4. 17. 21:10

안녕하세요. 명월입니다.


이 글은 C++에서 사용되는 문자열 타입(LPSTR, LPCSTR, LPCTSTR, LPCWSTR), 메모리 누수(memory leak) 체크하는 방법과 UTF8 변환하는 방법에 대한 글입니다.


제가 예전에 C++를 사용하다가 C#과 Java를 사용했을 때 문자열 클래스(String)가 있는 걸로 C#과 Java가 정말 편한 언어다라고 생각할 정도로 C++에서는 이 문자열이 까다로웠습니다.

C++에서 string의 객체가 있습니다. 지금은 어떤 지 모르겠는데, 이 string 객체는 메모리 릭이 자주 발생해서 잘 사용하지 않습니다.

#include <stdio.h>  
#include <iostream>
#include <Windows.h>
#include <string>
// 메모리 릭 체크하는 함수를 사용하기 위한 라이브러리 _CrtDumpMemoryLeaks();
#include <crtdbg.h>
// 메모리 릭을 콘솔에 표시하기 위한 함수
#if _DEBUG 
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__) 
#define malloc(s) _malloc_dbg(s, _NORMAL_BLOCK, __FILE__, __LINE__) 
#endif
using namespace std;
// 테스트를 위한 클래스
class Node {
  // c++의 string 객체
  string test;
};
// 실행 함수
int main()
{
  // Node를 선언
  Node a;
  // 메모리 초기화
  memset(&a, 0x00, sizeof(Node));
  
  // 종료 직전에 릭 확인을 한다.
  _CrtDumpMemoryLeaks();
  return 0;
}

여기서 잠깐 메모리 릭에 대해서 설명하면 Java나 C#의 경우는 GC(Garbage collection)이라고 메모리를 자동으로 정리해 주는 머신이 있습니다.

그런데 C++에는 그런 메모리를 자동으로 정리해주는 기능이 없습니다. 즉, 프로그램이 메모리를 할당(변수를 힙에 선언)하면 반드시 해제 해야 합니다.

해제를 하지 않으면 그대로 힙에 메모리가 쌓이게 되는데, 해제를 하지 않으면 프로그램이 장시간 실행이 되면 이 메모리가 쌓이게 됩니다. 시스템의 메모리는 무한 자원이 아닌 유한 자원이기 때문에 언젠가는 메모리가 꽉차서 컴퓨터가 다운되는 현상이 발생하게 됩니다.

클라이언트는 그나마 상황이 나은데, 몇날 몇일 기동하는 서버라고 하면 이는 심각한 문제가 발생합니다.


그래서 제대로 해제를 하는 데, 해제를 하는 데 있어서 포인터를 잃어버려 해제를 할 수 없는 상황이 발생하게 되면 메모리 릭(메모리를 해제할 수 없는 상황)이 발생하게 되어 버립니다.

위 string객체는 char[] 배열과 const char*등을 자연스럽게 연결하기 위해 아마도 동적 할당(heap 메모리에 변수를 할당하는 것)하는데.. 이것을 개발자가 해제를 할 수 있는 타이밍이 없습니다.


그래서 릭이 발생하게 됩니다. 릭이 발생하는 객체라고 하면 C++에서는 사용할 수 없는 객체입니다.

그러므로 C++의 string은 사용을 권장하지 않습니다.

위를 보니 char 배열은 릭이 발생하지 않습니다.


다시 문자열로 돌아와서 C++에서는 두가지 타입의 문자열을 취급합니다.

먼저 ascii코드로 이루어져 있는 char형식과 unicode 형식으로 이루어져 있는 unicode로 다룰 수 있습니다.

window에서는 멀티바이트 형식이 존재합니다. (멀티 바이트가 2000년도에 다국어 기능 문자열로 멀티바이트를 밀었는데, 결국은 unicode가 표준이 되었습니다. 지금은 잘 쓰지 않지만, 90년대 2000년대 소스에는 멀티바이트를 사용하는 소스도 많습니다.)

char형식으로는 한글이 지원되지 않습니다.(ascii코드에는 한글이 없습니다.)

그래서 한글을 사용하려면 unicode를 사용해야 하는데 이게 wchar_t타입 형식입니다.


여기서 문자열을 다루는 방법에는 또 두가지 방법이 있습니다. 배열에 넣는 방법과 포인터로 다루는 방법이 있습니다.

#include <stdio.h>  
#include <iostream>
#include <Windows.h>
// 메모리 릭 체크하는 함수를 사용하기 위한 라이브러리 _CrtDumpMemoryLeaks();
#include <crtdbg.h>
// 메모리 릭을 콘솔에 표시하기 위한 함수
#if _DEBUG 
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__) 
#define malloc(s) _malloc_dbg(s, _NORMAL_BLOCK, __FILE__, __LINE__) 
#endif
using namespace std;
// 실행 함수
int main()
{
  // 배열로 데이터를 저장 (ascii)
  char type1[1024] = "Hello world1";
  // 배열로 데이터를 저장 (unicode)
  wchar_t type2[1024] = L"Hello world2";
  // 포인터로 데이터를 저장 (ascii)
  const char* type3 = "Hello world3\0";
  // 포인터로 데이터를 저장 (unicode)
  const wchar_t* type4 = L"Hello world4\0";
  // 콘솔 출력
  cout << "type1 - " << type1 << endl;
  wcout << "type2 - " << type2 << endl;
  cout << "type3 - " << type3 << endl;
  wcout << "type4 - " << type4 << endl;
  // 종료 직전에 릭 확인을 한다.
  _CrtDumpMemoryLeaks();
  return 0;
}

이 두 차이가 사실 문자열은 배열로 넣어야 다루기가 쉽습니다. 근데 포인터로 직접 문자열을 관리가 가능한데 이때 const 키워드 수정할 수 없는 상수형 타입 설정이 필요합니다.

이 뜻은 C언어는 소스 레벨(메크로)과 Runtime 레벨의 값이 존재합니다. 소스 레벨은 말 그대로 소스 페이지 *.cpp를 문서로 취급하여 이를 전체 text타입 객체로 인식합니다.

거기에서 "Hello world3"를 직접 포인터로 연결한 것으로 Runtime레벨에서 소스 레벨의 값을 수정할 수 없으니 const가 붙게 되는 것입니다.

그래서 배열로 담게 되면 소스 레벨에서 런타임 레벨로 메모리 복사를 하게 되므로 const 없이 사용이 가능합니다. 근데 이것도 문제점이 존재합니다.

위 소스는 워낙 짧은 소스이니 헤갈리는게 없지만 변수를 재사용하게 된다면 배열의 사이즈가 작아서 문자가 짤리는 경우가 간혹 발생합니다. 이는 프로그램의 잘못이 아닌 개발자의 미스로 발생하는 것입니다.


그래서 이게 개발자들의 취향에 따라 배열과 포인터를 사용합니다만, 저는 포인터를 다루는게 편해서 const char* 형식을 자주 사용합니다.


여기서 다시 Windows.h 함수를 보게 되면 LPSTR, LPCSTR, LPCTSTR, LPCWSTR 형식으로 된 문자열 타입이 존재합니다.


이게 C++를 잘 모르는 사람이 보게 되면 왜 이리 문자열 타입이 많아 하고 오히려 이것 때문에 C++은 어렵다라고 하는 사람이 있을 정도입니다.

사실 이 차이는 엄청 간단합니다.

LP는 포인터의 의미입니다. 정확히는 long pointer인데 앞에 long이 왜 붙었나 하면 예전 16비트 시절의 메모리 포인터와 32비트에 메모리 포인트의 차이라고 합니다.

저도 PSTR의 형식을 사용하는 것을 본적이 없기 때문에 그냥 LP로 시작하는 것을 사용하면 됩니다. 16비트 시절이면...

그리고 C는 const의 의미입니다. W는 unicode, T는 멀티바이트의 의미입니다. STR은 char입니다.

즉 LPSTR은 char*이고 LPCSTR*은 const char*, 멀티 바이트도 const char*를 사용합니다. 멀티바이트는 이제 잘 사용하지 않으니 그냥 넘어갑니다.

LPCWSTR은 const wchar_t*의 의미가 됩니다.

여기서 그럼 LPWSTR은? 그렇습니다. wchar_t*가 됩니다.


여기까지 알게되면 이제 C++에서 문자열을 사용하는데 대충 눈에 들어오게 됩니다. 왜냐하면 거의 const char*나 const wchar_t*를 사용하는게 아니고 LPCSTR이나 LPCWSTR로 사용하기 때문입니다.

#include <stdio.h>  
#include <iostream>
#include <Windows.h>
// 메모리 릭 체크하는 함수를 사용하기 위한 라이브러리 _CrtDumpMemoryLeaks();
#include <crtdbg.h>
// 메모리 릭을 콘솔에 표시하기 위한 함수
#if _DEBUG 
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__) 
#define malloc(s) _malloc_dbg(s, _NORMAL_BLOCK, __FILE__, __LINE__) 
#endif
using namespace std;
// 실행 함수
int main()
{
  // const char*와 같다
  LPCSTR type1 = "Hello world1\0";
  // const wchar_t*와 같다
  LPCWSTR type2 = L"Hello world2\0";
  // 콘솔 출력
  cout << "type1 - " << type1 << endl;
  wcout << "type2 - " << type2 << endl;
  // 종료 직전에 릭 확인을 한다.
  _CrtDumpMemoryLeaks();
  return 0;
}

이제 여기까지 오면 문자열에서 가장 어려운게 ascii코드에서 unicode로 변환하는 방법과 최근에 가장 많이 사용하는 UTF8타입으로 변환하는 것입니다.

#include <stdio.h>	
#include <iostream>
#include <Windows.h>
// 메모리 릭 체크하는 함수를 사용하기 위한 라이브러리 _CrtDumpMemoryLeaks();
#include <crtdbg.h>
// 메모리 릭을 콘솔에 표시하기 위한 함수
#if _DEBUG 
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__) 
#define malloc(s) _malloc_dbg(s, _NORMAL_BLOCK, __FILE__, __LINE__) 
#endif
using namespace std;
// 실행 함수
int main()
{
  // 변수 선언
  LPCSTR type1 = "Hello world\0";
  LPCWSTR type2 = L"Hello world2\0";
  //type2의 wchar_t타입을 char로 변환하기 위한 변수.
  // const char는 소스 레벨의 값을 가져오기 때문에 const가 필요하지만, memcpy하는 값이기 때문에 const를 사용하지 않느다.
  char type3[BUFFERSIZE];
  //type1의 char타입을 wchar_t로 변환하기 위한 변수.
  wchar_t type4[BUFFERSIZE];
  // char를 wchar_t로 변환하는 함수
  // 파라미터 순서는 타입, 0, 소스 값, 소스 값 길이, 대상, 대상 길이
  MultiByteToWideChar(CP_ACP, 0, type1, strlen(type1), type4, BUFFERSIZE);
  // wchar_t를 char로 변환하는 함수
  // 파라미터 순서는 타입, 0 소스 값, 소스 값 길이(메모리 길이임 글자 길이가 아님), 대상, 대상 길이, NULL, NULL
  WideCharToMultiByte(CP_ACP, 0, type2, wcslen(type2) * 2, type3, BUFFERSIZE, NULL, NULL);
  // 콘솔 출력
  cout << "type1 - " << type1 << endl;
  wcout << "type2 - " << type2 << endl;
  cout << "type3 - " << type3 << endl;
  wcout << "type4 - " << type4 << endl;
  // 종료 직전에 릭 확인을 한다.
  _CrtDumpMemoryLeaks();
  return 0;
}

여기서 이제 UTF8를 변환하는 방법입니다.

C++에서는 UTF8를 그냥 멀티바이트 타입으로 취급하기 때문에 유니코드를 변환할 때는 MultiByteToWideChar, UTF8로 변환할 때는 WideCharToMultiByte를 사용합니다.

#include <stdio.h>  
#include <iostream>
#include <Windows.h>
// 메모리 릭 체크하는 함수를 사용하기 위한 라이브러리 _CrtDumpMemoryLeaks();
#include <crtdbg.h>
// 메모리 릭을 콘솔에 표시하기 위한 함수
#if _DEBUG 
#define new new(_NORMAL_BLOCK, __FILE__, __LINE__) 
#define malloc(s) _malloc_dbg(s, _NORMAL_BLOCK, __FILE__, __LINE__) 
#endif
using namespace std;
// 실행 함수
int main()
{
  // unicode 값
  LPCWSTR type1 = L"Hello world\0";
  // UTF8형식으로 변환
  char type2[BUFFERSIZE];
  // 다시 unicode로 변환
  wchar_t type3[BUFFERSIZE];
  // unicode에서 utf8로 변환, 타입을 CP_ACP가 아닌 CP_UTF8를 사용한다.
  WideCharToMultiByte(CP_UTF8, 0, type1, wcslen(type1) * 2, type2, BUFFERSIZE, NULL, NULL);
  // utf8에서 unicode로 변환, 타입을 CP_ACP가 아닌 CP_UTF8를 사용한다.
  MultiByteToWideChar(CP_UTF8, 0, type2, strlen(type2), type3, BUFFERSIZE);
  // 콘솔 출력
  wcout << "type1 - " << type1 << endl;
  cout << "type2 - " << type2 << endl;
  wcout << "type3 - " << type3 << endl;
  // 종료 직전에 릭 확인을 한다.
  _CrtDumpMemoryLeaks();
  return 0;
}

타입의 관한 전처리 값은 다음과 같습니다.

여기까지 C++에서 사용되는 문자열 타입(LPSTR, LPCSTR, LPCTSTR, LPCWSTR), 메모리 누수(memory leak) 체크하는 방법과 UTF8 변환하는 방법에 대한 글이었습니다.


궁금한 점이나 잘못된 점이 있으면 댓글 부탁드립니다.