백택 (bektekk@yahoo.co.kr)
홈페이지 : http://bektekk.wo.to
문자열(스트링) 전격 분석 2부 1강
본강좌는 코드 프로젝트에 제가 좋아하는 프로그래머인 Michael Dunn의 강좌
The Complete Guide to C++ Strings, Part I 과 II 를 번역한 글입니다.
최대한 의역을 하려고 노력했지만, 이런쪽의 경험이 부족하다 보니 많이 모자란
강좌가 되겠지만, 많은 도움이 되셨으면 합니다.
기타 문의 사항이나 질문은 쪽지나 메일을 이용해 주셨으면 합니다.
본강좌는 제 홈페이지(위의 링크)를 통해서도 보실수 있습니다.
이번강 까지도 좀 지루하겠네요.
다음강을 마지막으로 실제 클래스들을 다루어 보겠습니다.
소개
C스타일 스트링은 에러를 유발하기 쉽고, 관리하기가 매우 까다로울뿐만 아니라, 해커에게 오버런 버그를 노출하는 타겟이 될 수도 있기 때문에, 수많은 스트링 랩퍼 클래스들이 생겨났다. 하지만 불행히도 어떤 상황에서 어떤 클래스를 써야하고 혹은 C스트링을 어떻게 랩퍼 클래스를 이용해 처리해야 하는지는 가끔씩 우리를 해깔리게 한다.
이번 강좌 2부의 내용은 Win32 API, MFC, STL, WTL과 VC런타임 라이브러리의 모든 스트링 랩퍼 클래스들을 다루고 있다. 각 클래스는 어떻게 생성하고 사용하며, 각각 어떻게 변환할수 있는지 설명할 것이다.
2부의 내용을 제대로 소화하기 위해서는 1부 1,2강에서 설명한 캐릭터 타입들과 인코딩 방식에 대한 충분한 이해가 필요할 것이다.
스트링 클래스의 가장 중요한 규칙
만약 문서화되어 있어 형변환에 문제가 없다는 확신이 없으면 강제 형변환은 피할것.
어떤 스트링랩퍼 클래스 X와 또다른 클래스 Z사이의 형변환에 관한 내용은 특히 초급 프로그래머들이 많이 궁금해 하는 내용이다. 주로 초급 프로그래머들은 강제 형변환을 시도한 후 왜 제대로 동작하지 않는가에 대해 많이 궁금해 하곤 한다. 수많은 스트링 타입들, 특히 BSTR 같은 타입은 명확히 문서화 되어있지 않은 게 현실이다. 따라서 많은 사람들은 쉽게 잘못된 코드를 작성할 가능성이 많았다.
무엇보다 중요한 점은 랩퍼클래스에서 형변환 연산자를 재정의 하지 않았다면 형변환은 사실 스트링 사이에서 아무 일도 하지 않는다. 그래서 만약 아래와 같은 코드를 작성 했다면:
void SomeFunc ( LPCWSTR widestr ); main() { SomeFunc ( (LPCWSTR) "C:\\foo.txt" ); // WRONG! }
위의 코드는 백이면 백 잘못된 결과를 초래할 것이다. 사실 위의 코드는 에러 없이 컴파일 된다. 하지만 컴파일이 된다고 그 코드가 옳바르다고 장담할수는 없는 것이다.
강의를 진행하면서 어떤 형변환이 옳바른 것인지 설명하도록 하겠다.
C-style strings and typedefs
지난 강의에서 살펴 보았듯이, Win API는 TCHAR 방식으로 정의되 있다. 그것은 컴파일 시에 문맥에 따라 MBCS방식으로 전환될수도 있고, 유니코드가 될수도 있다. 편의를 위해 지난 강의에 보여줬던 표를 다시 보여 주도록 하겠다.
타입 | MBCS환경에서 | 유니코드 환경에서 |
---|---|---|
WCHAR |
wchar_t |
wchar_t |
LPSTR |
0으로 끝나는 char형의 문자열(char* ) |
0으로 끝나는 char형의 문자열 (char* ) |
LPCSTR |
0으로 끝나는 const char형의 문자열 (const char* ) |
0으로 끝나는 const char형의 문자열 (const char* ) |
LPWSTR |
0으로 끝나는 유니코드형의 문자열 (wchar_t* ) |
0으로 끝나는 유니코드형의 문자열 (wchar_t* ) |
LPCWSTR |
0으로 끝나는 const 유니코드형의 문자열 (const wchar_t* ) |
0으로 끝나는 const 유니코드형의 문자열 (const wchar_t* ) |
TCHAR |
char |
wchar_t |
LPTSTR |
0으로 끝나는 TCHAR형의 문자열 (TCHAR* -> char* ) |
0으로 끝나는 TCHAR형의 문자열 (TCHAR*->wchar_t* ) |
LPCTSTR |
0으로 끝나는 const TCHAR형의 문자열 (const TCHAR* ) |
0으로 끝나는 const TCHAR형의 문자열 (const TCHAR* ) |
타입을 하나 추가 하자면 OLECHAR
을 들수 있겠다. 이 타입은 주로 자동화 인터페이스에서 사용된다. 이 타입은 보통 wchar_t
으로 정의되 있으나, 셋팅을 변경하거나 #define 문으로 OLE2ANSI
를 정의하면 단순 char
타입으로 전환된다. 하지만 사실상 요즘엔 OLE2ANSI
를 적용할 이유는 없다. (사실, MFC3 버젼에서 사용되어졌던 것이다. 구시대의 유물^^), 따라서 지금부터 그냥 단순히 OLECHAR
타입을 유니코드로 간주할 것이다.
아래의 표는 OLECHAR
와 관계된 typedef 문으로 정의된 데이터 타입을 보여준다. :
타입 | 의미 |
---|---|
OLECHAR |
유니코드 케릭터 (wchar_t ) |
LPOLESTR |
유니코드 스트링 (OLECHAR* ) |
LPCOLESTR |
const 형 유니코드 스트링 (const OLECHAR* ) |
문자열을 다룰 때 유니코드 MBCS 방식에 관계없이 일관된 표현을 할수 있게 해주는 두가지 마크로가 있다. _T 마크로는 지난 강의에서도 다루었던 부분이다. :
마크로 | 의미 |
---|---|
_T(x) |
L 유니코드빌드일때 L을 앞에 주가해 준다. |
OLESTR(x) |
LPOLESTR 타입으로 만들기 위해 L을 앞에 추가해 준다. |
또한 _T에서 변형된 형태의 하지만 같은 역활을 하는 몇몇 마크로 들이 더 있다. -- TEXT
, _TEXT
, __TEXT
, and __T
이 마크로 들은 모두 같은 일을 한다.
COM에서의 스트링 - BSTR
많은 자동화객체 인터페이스나 COM 인터페이스는 스트링으로 BSTR 타입을 사용한다. 하지만 BSTR타입은 다루기 매우 까다롭고, 에러를 유발하기 쉽다. 따라서 BSTR을 이번 파트에 다뤄 보도록 하겠다.
BSTR
타입은 파스칼 스타일의 스트링과 C 스트링 사이의 잡종 즉 서로 짬뽕되서 생긴 타입이다. 파스칼에서는 문자열타입에 그 길이가 내부적으로 저장된다. 하지만 C스트링에서는 마지막 제로바이트를 통해서 그 문자열의 끝을 알수있게끔 되어 있다. 사실 BSTR
타입은 문자일 길이를 문자열 시작 바로 전에 저장하고 이어서 유니코드 스트링을 저장하는 방식의 타입니다. 또한 제로바이트로 그 끝을 표시한다. 아래는 "Bob" 이라는 BSTR 타입의 스트링의 메모리 구조를 보여준다.:
06 00 00 00 |
42 00 |
6F 00 |
62 00 |
00 00 |
|
|
|
|
|
눈치채셨다 시피 문자열의 길이가 DWORD타입으로 (즉 4바이트) 실제 문자 앞에 저장된다. 하지만 이는 마지막 00 00의 제로바이트를 포함하지 않은 길이 이다. 위의 경우 "Bob" 문자열은 총 6바이트의 3개의 유니코드 캐릭터를 가지고 있다. 길이 정보를 포함하는 이유는 COM 라이브러리가 다른 곳으로 마샬링 될때 얼마나 많은 바이트를 보내야 하는지 그 정보가 필요하기 때문이다. (사실, BSTR
은 단지 스트링뿐만 아니라 임의의 어떤 데이타가 들어 있어도 상관이 없다.)
BSTR
변수는 C++에서 첫번째 캐릭터를 가리키는 포인터 변수 이다. BSTR
타입은 이렇게 정의 되있다. :
typedef OLECHAR* BSTR;
불행히도 이런방식의 정의는 많은 문제를 유발할수 있다. 위에서도 설명했듯이 실제로는 BSTR은 유니코드 스트링과는 다른 특성을 가진 타입이다. 따라서 BSTR과 LPOLESTR은 마음대로 섞어 써도 컴파일러는 에러를 발생하지 않는다. LPOLESTR을 인자로 받는 함수에 BSTR 타입을 전달하는 것은 안전하다. 그렇지만 그 반대의 경우는 다르다. 따라서 함수가 받는 인자의 타입을 정확히 알고 정확히 전달하는 것이 중요하다.
BSTR 타입을 받는 함수에 LPOLESTR을 전달하는것이 왜 안전하지 못하냐 하면, BSTR이 가리키는 메모리 바로 앞 4바이트는 그 문자열의 길이를 포함하는 정보를 담고 있어여 한다. 아마 BSTR을 받는 함수에서는 그 정보가 포함되 있다는 가정하에 그 정보를 이용할 것이다. 하지만 LPOLESTR 타입에는 그러한 정보는 없다. 이는 안전하지 못한 결과를 낳을 것이다. 위에서 말했다 시피 COM객체에서는 BSTR 의 문자열 앞에 존재하는 문자열길이 정보를 이용해 그 만큼의 데이터를 전송한다고 있다. BSTR대신 LPOLESTR을 전달하면 얼마만큼의 바이트가 전송될지는 아무도 장담할 수가 없게 되는 것이다.
따라서 BSTR을 다루기 위한 몇몇 API들이 존재하지만 그중 가장 중요한 것은 두가지 이다. 하나는 BSTR을 생성시키기 위한 것이고, 다른하나는 제거하기 위한 것이다. BSTR을 생성하는 함수는 SysAllocString()
이고 제거하는것은 SysFreeString()
이다. SysAllocString()
은 매개변수로 받은 유니코드 스트링을 BSTR형태로 만들어 주는 역할은 한다. (새로운 메모리를 할당한 후에 아마 문자열 길이를 계산해서 문자열 앞에 그 길이정보를 추가해 주는 정도의 일을 할 것이다.) 반면 SysFreeString()
은 BSTR
을 메모리에서 제거하는 역할을 한다.
BSTR bstr = NULL; bstr = SysAllocString ( L"Hi Bob!" ); if ( NULL == bstr ) // 메모리가 부족 // bstr을 마음껏 사용^^ SysFreeString ( bstr );
사실, 스트링 하나 사용하겠다고 이런 일련의 함수를 계속적으로 사용하는 것은 굉장히 피곤하다. 따라서 자연스럽게 메모리 할당, 제거를 자동적으로 해주는 몇몇 랩퍼 클래스들이 생겨나게 됐다. 그에 대해서 뒤에 살펴보기로 하자.