Рихтер Дж., Назар К. - Windows via C C++. Программирование на языке Visual C++ - 2009
.pdf24 Часть I Материалы для обязательного чтения
Рис. 2-2. Состояние переменных перед вызовом _tcscpy_s
Поскольку длина строки «1234567890», копируемой в буфер szBuffer, точно равна размеру буфера (10 символов), концевой символ строки, «\0», в буфер не уместится. Не думайте, что значение result будет усечено (вызовом STRUNCATE) и последний символ, «9», просто не будет скопирован в буфер. Вместо этого возвращается ERANGE и переменные приходят в состояние, показанное на рис. 2-3.
Рис. 2-3. Состояние переменных после вызова _tcscpy_s
Есть и еще один «побочный эффект», который нельзя увидеть, не проанализировав содержимое ячеек памяти, расположенных сразу за szBuffer (рис. 2-4).
Рис. 2-4. Содержимое szBuffer после неудачного вызова _tcscpy_s
Первый символ установлен в «\0», а остальные байты теперь содержат значение 0xfd. В результате результирующая строка оказалась усеченной до пустой строки, а остальные байты буфера — занятыми значениями-заполнителями (0xfd).
Примечание. Если вам интересно, почему ячейки памяти между переменными заполнены значениями 0xcc (см. рис. 2-4), скажу: из-за автоматического обнаружения компилятором переполнения буфера во время выполнения (оно включается флагами /RTCs, /RTCu и /RTC1). Если код компилируется без флагов /RTCx, переменные sz* будут располагаться в памяти «вплотную». Помните, что всегда следует устанавливать эти флаги при компиляции — это позволит обнаружить еще не выявленные ошибки из-за переполнения буфера на ранних стадиях цикла разработки.
Глава 2. Работа с символами и строками.docx 25
Дополнительные возможности при работе со строками
Помимо безопасных строковых функций, библиотека С поддерживает ряд новых функций, предоставляющих дополнительные возможности при манипулировании строками. Например, с их помощью можно управлять символами-заполнителями и способом усечения строк. Естественно, поддерживаются как ANSI- (А), так и Unicode-версии (W) этих функций. Вот прототипы для некоторых их них:
HRESULT StringCchCat(PTSTR pszDest, size_t cchDest, PCTSTR pszSrc); HRESULT StringCchCatEx(PTSTR pszDest, size_t cchDest, PCTSTR pszSrc,
PTSTR *ppszDestEnd, size_t *pcchRemaining, DWORD dwFlags); HRESULT StringCchCopy(PTSTR pszDest, size_t cchDest, PCTSTR pszSrc); HRESULT StringCchCopyEx(PTSTR pszDest. size_t cchDest, PCTSTR pszSrc,
PTSTR *ppszDestEnd, size_t *pcchRemaining, DWORD dwFlags);
HRESULT StringCchPrintf(PTSTR pszDest, size_t cchDest,
PCTSTR pszFormat, …);
HRESULT StringCchPrintfEx(PTSTR pszDest, size_t cchDest,
PTSTR *ppszDestEnd, size_t *pcchRemaining, DWORD dwFlags,
PCTSTR pszFormat, …);
Несложно заметить строку «Cch» в именах показанных здесь методов: это сокращение от «Count of characters», обычно это значение получают при помощи макроса _countof. Также поддерживаются функции с именами, содержащими строку «Cb», такие как StringCbCat(Ex), StringCbCopy(Ex) и StringCbPrintf(Ex).
Эти функции принимают в виде параметра размер, выраженный в байтах, а не символах. Как правило, его значение получают с помощью оператора sizeof. Все эти функции возвращают HRESULT с одним из значений, перечисленных в табл.
2-2.
Табл. 2-2. Значения HRESULT для безопасных строковых функций
Значение |
Описание |
|
S_OK |
Успешный вызов. В целевом буфере находится исходная |
|
строка, оканчивающаяся знаками «\0» |
||
|
||
STRSAFE_E_INVALID |
Неудачный вызов. В параметрах передан NULL |
|
_PARAMETER |
|
|
STRSAFE_E_ |
Неудачный вызов. Исходная строка не уместилась в целе- |
|
INSUFFICIENT_ |
вом буфере |
|
BUFFER |
|
В отличие от безопасных функций (функций с суффиксом _s в именах), эти функции усекают строку, если она не умещается в буфер. Об этом сви-
детельствует возврат STRSAFE_E_INSUFFICIENT_BUFFER. Из кода
StrSafe.h видно, что этот код имеет значение 0x8007007а, и макрос SUCCEEDED/FAILED считает его неудачей вызова. Однако в этом слу-
26 Часть I Материалы для обязательного чтения
чае копируется часть содержимого исходного буфера, способная уместиться в целевом буфере, при этом последним символом скопированной строки становится «\0». Таким образом, если в предыдущем примере использовать StringCchCopy вместо _tcscpy_s, то в szBuffer оказалась бы строка «012345678». В зависимости от поставленной задачи, усечение строки при копировании может быть допустимо, а, может, и нет. Именно поэтому данная ситуация по умолчанию считается неудачей вызова. Например, при сборке пути путем конкатенации строк усечение сделает полученный результат бесполезным. В случае же подготовки текстового сообщения для пользователя усечение может оказаться приемлемым вариантом. В любом случае, вам решать, что делать с усеченным при копировании результатом.
И последнее (по порядку, но не по важности): у многих показанных выше функций есть расширенная (Ex) версия. Расширенные версии принимают три дополнительных параметра, описанных в табл. 2-3.
Табл. 2-3. Параметры расширенных функций
Параметр |
Описание |
size_t* pcchRemaining |
Указатель на переменную, представляющую число незанятых симво- |
|
лов в целевом буфере (без учета копируемого концевого нуля, \0). На- |
|
пример, при копировании одного символа в буфер длиной 10 символов |
|
будет возвращено значение 9, из которых доступны лишь 8 (остальные |
|
символы будут усечены). Если pcchRemaining = NULL, число незаня- |
|
тых символов не возвращается |
LPTSTR* ppszDestEnd |
Значение ppszDestEnd, отличное от NULL, указывает на «\0», завер- |
|
шающий строку в целевом буфере |
DWORD dwFlags |
Одно или несколько значений, разделенных знаком «|» |
STRSAFE_FILL_ |
При успешном вызове использует младший байт dwFlags для заполне- |
BEHIND_NULL |
ния области буфера, оставшейся незанятой (после концевого «\0»). |
|
Подробнее см. в примечании о STRSAFE_FILL_BYTE после таблицы |
STRSAFE_IGNORE_ |
Заставляет обращаться с указателями на строки, содержащими NULL, |
NULLS |
как с пустыми строками (TEXT(“”)) |
STRSAFE_FILL_ |
При неудачном вызове использует младший байт dwFlags для заполне- |
ON_FAILURE |
ния целевого буфера (за исключением концевого «\0»), подробнее см. в |
|
описании STRSAFE_FILL_BYTE после таблицы. В случае ошибки |
|
STRSAFE_E_INSUFFICIENT_BUFFER все символы возвращаемой |
|
строки замещаются байтами-заполнителями |
STRSAFE NULL_ |
При неудачном вызове первым символом целевого буфера становится |
ON_FAILURE |
«\0», то есть в буфер заносится пустая строка (TEXT(“”)). В случае |
|
ошибки STRSAFE_E_INS U FFI-CIENT_BUFFER любая усеченная |
|
строка будет перезаписана |
|
Глава 2. Работа с символами и строками.docx 27 |
|
|
Параметр |
Описание |
STRSAFE_NO_ |
Как и в случае STRSAFE_NULL_ON_FAILURE, при сбое функции в |
TRUNCATION |
целевой буфер заносится пустая строка (ТЕХТ (“”)). В случае ошибки |
|
STRSAFE_E_INSUFFICIENT_BUF-FER любая усеченная строка будет |
|
перезаписана |
Примечание. Даже если установлен флаг STRSAFE_NO_TRUNCATION, в целевой буфер будут скопированы все символы исходной строки, которые в нем уместятся. Далее первый и последний символы в целевом буфере устанавливаются в «\0». Это важно только в ситуациях, когда по соображениям безопасности следует уничтожить ненужные данные.
В завершение вернемся к комментарию на стр. 21. Как видно из рис. 2-4, все ячейки целевого буфера, от «\0» и до конца, заменены значениями 0xfd. Расширенные (Ex) версии этих функций позволяют при желании отменить заполнение буфера, весьма затратное (особенно в случае большого буфера) в плане системных ресурсов. Если добавить STRSAFE_FILL_BEHIND_ NULL к dwFlag, остальные символы будут установлены в «\0». Если же заменить
STRSAFE_FILL_BEHIND_NULL макросом STRSAFE_FILL_BYTE, для заполне-
ния незанятой части целевого буфера будет использоваться заданный байт.
Строковые функции Windows
Windows предлагает внушительный набор функций, работающих со строками. Многие из них, такие как lstrcat и lstrcpy, считаются устаревшими и не рекомендуются к использованию, поскольку не позволяют обнаружить ошибок, возникающих из-за переполнения буфера. Кроме того, в ShlwApi.h определен ряд удобных строковых функций, таких как StrFormatKBSize и StrFormatByteSize, форматирующих числовые значения, связанные с работой операционной системы. Подробнее о строковых функциях оболочки см. по ссылке http://msdn2.microsoft.com/en-us/library/ms538658.aspx.
Операция сравнения строк используется при проверке равенства, а также при сортировке. Наилучшие функции для этой операции — CompareString(Ex) и CompareStringOriginal. CompareString(Ex) используется для сравнения строк, которые затем будут показаны пользователю, с учетом языковых особенностей. Вот про-
тотип функции CompareString.
int CompareString( LCID locale, DWORD dwCmdFlags, PCTSTR pString1,
28 Часть I Материалы для обязательного чтения
int cch1, PCTSTR pString2, int cch2);
Она сравнивает две строки. Первый параметр задает так называемый идентификатор локализации (locale ID, LCID) — 32-битное значение, определяющее конкретный язык, С помощью этого идентификатора CompareString сравнивает строки с учетом значения конкретных символов в данном языке. Так что она действует куда осмысленнее, чем функции библиотеки С. Однако, эта операция существенно медленнее сравнения по порядку. Получить LCID можно вызовом
Windows-функции GetThreadLocale:
LCID GetThreadLocale();
Второй параметр функции CompareString указывает флаги, модифицирующие метод сравнения строк. Допустимые флаги перечислены в следующей таблице.
Табл. 2-4. Флаги функции CompareString
Флаг |
Действие |
|
|
NORM_IGNORECASE |
Различия в регистре букв |
игнорируются |
LINGUIS- |
|
TIC_IGNORECASE |
|
|
NORM_IGNOREKANATYPE |
Различия между знаками хираганы и катаканы игнорируются |
||
NORM_IGNORENONSPACE |
Знаки, отличные от пробелов, |
игнорируются |
LINGUIS- |
|
TIC_IGNOREDIACRITIC |
|
|
NORM_ IGNORESYMBOLS |
Символы, отличные от алфавитно-цифровых, игнорируются |
||
NORM_IGNOREWIDTH |
Разница между одно- и двухбайтовым представлением одного |
||
|
и того же символа игнорируется |
|
|
NORM_STRDVGSORT |
Знаки препинания обрабатываются так же, как и символы, от- |
||
|
личные от алфавитно-цифровых |
|
|
Остальные четыре параметра CompareString задают две строки и их длину в символах (а не в байтах!), соответственно. Если передать в параметре cch1 отрицательное значение, функция рассчитывает длину строки pString1, предполагая, что эта строка оканчивается нулем. То же верно и для параметра cch2 в отношении строки pString2. Если вам нужны дополнительные возможности, связанные с особенностями различных языков, взгляните на функции CompareStringEx.
Для сравнения строк при программной генерации строковых элементов (путей, разделов и параметров реестра, XML-атрибутов и элементов) применяют функцию CompareStringOrdinal:
int CompareStringOrdinal( PCWSTR p8tring1,
Глава 2. Работа с символами и строками.docx 29
int cchCount1, PCWSTR pString2, int cchCount2, BOOL bIgnoreCase);
Эта функция выполняет сравнение строк как строк программы, то есть без учета региональных параметров, и потому работает быстро. Это наиболее полезная функция, поскольку текст программы, как правило, не виден конечным пользователям. Учтите, что она работает только с Unicode-строками.
Функции CompareString и CompareStringOrdinal возвращают значения, кото-
рые не похожи на значения, возвращаемые функциями вида *cmp из библиотеки С.
CompareString(Ordinal) возвращает 0 в случае сбоя; CSTR_LESS_THAN (значение 1), если pString1 меньше pString2; CSTR_EQUAL (значение 2), если pString1 = pString2; и CSTR_GREATER_THAN (значение 3), если pString1 больше pString2. Можно немного упростить себе жизнь, если (после успешного вызова) вычесть 2 из возвращаемого значения — получится результат, соответствующий значению, возвращаемому функциями из библиотеки С (-1,0, и +1).
Почему Unicode?
Разрабатывая приложение, вы определенно должны использовать преимущества Unicode, поскольку они:
■упрощают локализацию приложений для распространения их на мировом рынке;
■позволяют распространять единственный двоичный EXEили DLL-файл, поддерживающий все языки;
■увеличивают эффективность приложений, поскольку с Unicode-строками код работает быстрее, используя при этом меньше памяти. Внутренне Windows работает только с Unicode-символами и строками, поэтому получив ANSI-символ или строку, Windows должна преобразовать ее в Unicode-эквивалент, выделив для этого память;
■дают возможность вызывать все современные Windows-функции (некоторые Windows-функции поддерживают только Unicode-символы и строки);
■облегчают интеграцию с СОМ, требующей использования Unicode-символов и строк;
■гарантируют простую интеграцию вашего кода с .NET Framework (также требующей применения Unicode-символов и строк);
■упрощают манипулирование собственными ресурсами приложений (которые также всегда хранятся в Unicode).
30 Часть I Материалы для обязательного чтения
Рекомендуемые приемы работы с символами и строками
В начале этого раздела я вкратце повторю правила, которых следует придерживаться при написании кода, а в конце дам несколько советов о том, как эффективнее манипулировать Unicode- и ANSI-строками. Даже если вы не планируете использовать Unicode сейчас, все же желательно создавать приложения в расчете на работу с Unicode. При разработке следуйте этим правилам:
■старайтесь думать о тестовых строках как о массивах символов, а не массивах значений типа chars или массивах байтов;
■используйте обобщенные типы данных (такие, как TCHAR/PTSTR) для текстовых строк и символов;
■используйте явные типы данных (такие как BYTE и PBYTE) для хранения байтов, указателей на байты и буферов;
■используйте для литералов макросы TEXT или _T но не применяйте их вперемешку — это вносит в код путаницу;
■пользуйтесь глобальной заменой (например, для замены PSTR на PTSTR);
■оптимизируйте выделение памяти для строк. Функции обычно принимают размер буфера, выраженный в символах, а не в байтах. Это означает, что переда-
вать следует _countof(szBuffer), а не sizeof(szBuffer). Кроме того, помните, что размер блока памяти, выделяемого для строк, включающих известное число символов, указывают в байтах. Следовательно, необходимо вызывать malloc(nCharacters* sizeof(TCHAR)), а не malloc(nCharacters). Это правило труднее всего запомнить, и компилятор никак не предупреждает о его нарушении, поэтому здесь придется весьма кстати макрос следующего вида:
#define chmalloc(nCharacters) (TCHAR*)malloc(nCharacters * sizeof(TCHAR))
■избегайте функций из семейства printf, особенно при использовании типов полей %s и %S для преобразования между ANSI и Unicode. Вместо них исполь-
зуйте MultiByteToWideChar и WideCharToMultiByte, как показано ниже, в разде-
ле о перекодировке строк;
■всегда определяйте символы UNICODE и _UNICODE одновременно либо не определяйте ни один из них.
Этих правил следует придерживаться при манипулировании строками:
■всегда используйте безопасные функции для манипулирования строками, в именах этих функций присутствует суффикс _s либо префикс StringCch. Последние применяют, когда нужно явно проконтролировать усечение строк, в остальных случаях используют безопасные функции с суффиксом _s в именах;
■не пользуйтесь небезопасными функциями библиотеки С для манипулирования строками (см. выше). Общее правило таково: не используй-
Глава 2. Работа с символами и строками.docx 31
те функцию, манипулирующую буфером, если она не принимает размер целевого буфера как параметр. Для этой цели библиотека С поддерживает новые функции, такие как memcpy_s, memmove_s, wmemcpy_s и wmemmove_s. Они доступны, если определен символ __STDC_WANT_SECURE_LIB__ (в CrtDefs.h он определен по умолчанию, не отменяйте это умолчание);
■используйте преимущества флагов компилятора /GS
(http://msdn2.microsoft.com/en-us/library/aa290051(VS.71).aspx) и /RTCs, позво-
ляющих автоматически обнаруживать переполнение буфера;
■не используйте для манипулирования строками методы Kernel32, такие как lstrcat и lstrcpy;
■в коде приходится манипулировать двумя типами строк. Первый тип - это фрагменты текста программы, включая имена файлов, пути, XML-элементы и атрибуты, а также разделы и параметры реестра. Для работы с такими строками используют функцию CompareStringOrdinal, поскольку она не учитывает региональные параметры и потому работает очень быстро. Это — преимущество, поскольку ни регион, ни страна, в которой запущено приложение, не влияет на такие строки. Другой тип строк — это строки, отображаемые в пользователь-
ском интерфейсе. С ними работают при помощи функции CompareString(Ex), которая учитывает региональные параметры при сравнении строк.
Здесь у вас нет выбора: профессиональные разработчики просто не могут использовать в коде небезопасные функции, манипулирующие буфером. Именно по этой причине во всех примерах, приведенных в этой книге, используются безопасные функции из библиотеки С.
Перекодировка строк из Unicode в ANSI и обратно
Windows-функция MultiByteToWideChar преобразует мультибайтовые символы строки в «широкобайтовые»;
int MultiByteToWideChar( UINT uCodePage, DWORD dwFlags,
PCSTR pMultiByteStr, int cbMultiByte, PWSTR pWideCharStr, int cchWideChar);
Параметр uCodePage задает номер кодовой страницы, связанной с мультибайтовой строкой. Параметр dwFlags влияет на преобразование букв с диакритическими знаками. Обычно эти флаги не используются, и dwFlags равен 0 (подробнее о допустимых значениях этого флага см. в документации
MSDN по ссылке http://msdn2.microsoft.com/en-us/library/ms776413.aspx).
Параметр pMultiByteStr указывает на преобразуемую строку, а cchMultiByte
32 Часть I Материалы для обязательного чтения
определяет ее длину в байтах. Функция самостоятельно определяет длину строки,
если cchMultiByte равен -1.
Unicode-версия строки, полученная в результате преобразования, записывается в буфер по адресу указанному в pWideCharStr. Максимальный размер этого буфера (в символах) задается в параметре cchWideChar. Если он равен 0, функция ничего не преобразует, а просто возвращает размер буфера, необходимого для сохранения результата преобразования (с учетом концевого символа «\0»), Обычно конверсия мультибайтовой строки в ее Unicode-эквивалент проходит так:
1.Вызывают MultiByteToWideChar, передавая NULL в параметре pWideCharStr, 0 в параметре cchWideChar и -1 — в параметре cbMultiByte.
2.Выделяют блок памяти, достаточный для сохранения преобразованной строки. Его размер получают путем умножения результата предыдущего вызова MultiByteToWideChar на sizeof(wchar_t).
3.Снова вызывают MultiByteToWideChar, на этот раз передавая адрес выделенного буфера в параметре pWideCharStr, а размер буфера, полученный при первом обращении к этой функции, — в параметре cchWideChar.
4.Работают с полученной строкой.
5.Освобождают блок памяти, занятый Unicode-строкой.
Обратное преобразование выполняет функция WideCharToMultiByte.
int WideCharToMultiByte( UINT uCodePage, DWORD dwFlags, PCWSTR pWideCharStr, int cchWideChar, PSTR pMultiByteStr, int cbMultiByte, PCSTR pDefaultChar,
PB00L pfUsedDefaultChar);
Она очень похожа на MultiByteToWideChar. И опять uCodePage определяет кодовую страницу для строки — результата преобразования. Дополнительный контроль над процессом преобразования дает параметр dwFlags. Его флаги влияют на символы с диакритическими знаками и на символы, которые система не может преобразовать. Такой уровень контроля обычно не нужен, и dwFlags приравнивается 0.
Параметр pWideChar указывает адрес преобразуемой строки, а cchWideChar задает ее длину в символах. Функция сама определяет длину исходной строки, ес-
ли cchWideChar равен -1.
Мультибайтовый вариант строки, полученный в результате преобразования, записывается в буфер, на который указывает pMultiByteStr. Параметр cchMultiByte определяет максимальный размер этого буфера в байтах. Передав нулевое значение в cchMultiByte, вы заставите функцию сообщить размер буфера, требуемого для записи результата. Обычно
Глава 2. Работа с символами и строками.docx 33
конверсия широкобайтовой строки в мультибайтовую проходит в той же последовательности, что и при обратном преобразовании, за одним исключением: сразу возвращается размер буфера (в байтах) для хранения результатов преобразования.
Очевидно, вы заметили, что WideCharToMultiByte принимает на два параметра больше, чем MultiByteToWideChar, это pDefaultChar и pfUsedDefaultChar. Функ-
ция WideCharToMultiByte использует их, только если встречает широкий символ, не представленный в кодовой странице, на которую ссылается uCodePage. Если его преобразование невозможно, функция берет символ, на который указывает pDefaultChar. Если этот параметр равен NULL (как обычно и бывает), функция использует системный символ по умолчанию. Таким символом обычно служит знак вопроса, что при операциях с именами файлов очень опасно, поскольку он является и символом подстановки.
Параметр pfUsedDefaultChar указывает на переменную типа BOOL, которую функция устанавливает как TRUE, если хоть один символ из широкосимвольной строки не преобразован в свой мультибайтовый эквивалент. Если же все символы преобразованы успешно, функция устанавливает переменную как FALSE. Обычно вы передаете NULL в этом параметре.
Подробнее эти функции и их применение описаны в документации Platform SDK.
Экспорт DLL-функций для работы с ANSI и Unicode
Эти две функции позволяют легко создавать ANSI- и Unicode-версии других функций, работающих со строками. Например, у вас есть DLL, содержащая функцию, которая переставляет все символы строки в обратном порядке. Unicodeверсию этой функции можно было бы написать следующим образом.
BOOL StringReverseW(PWSTR pWideCharStr, DWORD cchLength) {
// получаем указатель на последний символ в строке
PWSTR pEndOfStr = pWideCharStr + wcsnlen_s(pWideCharStr , cchLength) - 1; wchar_t cCharT;
// повторяем, пока не дойдем до середины строки while (pWideCharStr < pEndOfStr) {
//записываем символ во временную переменную cCharT = *pWideCharStr;
//помещаем последний символ на место первого
*pWideCharStr = *pEndOfStr;
//копируем символ из временной переменной на место,
*pEndOfStr = cCharT;