Введение
Занимаясь разработкой игрового движка, мне из некоторых соображений понадобилось написать набор функций для работы со строками, в том числе аналог библиотечной sprintf.
Собственно, моя реализация местами смехотворна и немного неэффективна, многим, конечно, покажется вообще глупой затеей. Но, если кому-нибудь это пригодится, хотя бы будет познавательно — вот и ладушки. Приступим?
Теория
Интерфейс функции выглядит так:
// Функция форматирования строки
char *StrFormat( const char *pText, ... );
Сразу бросается в глаза отличие от sprintf: функция не принимает указатель на буфер, где будет размещаться готовая строка. Вместо этого, она создает буфер автоматически и возвращает его. Это одна из причин, по которой «изобреталось колесо».
Троеточием обозначается список переменного числа параметров, с ним будем работать напрямую без va_args.
Теперь, если кто-нибудь еще не в курсе. Как получить значения переданных аргументов, если у них нет имен? Запросто. Переданные аргументы размещаются на стеке сразу за pText, поэтому их можно заполучить применяя указатель.
Вот как это делается:
- Возьмем указатель на первый известный аргумент — pText (в нашем случае это будет указатель на указатель). Это будет нашей базой, от которой будем отталкиваться;
- Сразу же сместимся на sizeof( char * ) — 4 байта — вправо, таким образом перейдя на начало первого аргумента из числа тех, что нам нужны;
- Из входной строки (pText) читаем текст, натыкаясь на спецификатор формата определяем тип аргумента. Например, для %i это int. Приводим «базовый» указатель к типу указатель-на-тип-аргумента и разыменовываем его;
- Сдвигаем указатель на количество байт, равное размеру типа аргумента, что мы уже изъяли;
- Profit!
Единственный нюанс связан с тем, что проверить и узнать формальное количество переданных аргументов в функцию нет никакой возможности. Остается полагаться только на то, что в форматируемой строке pText спецификаторы формата указаны верно.
А теперь алгоритм работы функции.
- Подсчитываем количество аргументов, делая холостой проход по строке pText;
- На втором проходе собираем информацию о аргументах: тип и значение;
- Выделяем память под новую строку, в которой будет сладываться результат. Копируем туда посимвольно ту строку, что нам передали заменяя %s и %c и т.д. значениями соответствующих аргументов.
- Выдаем результат
За очистку памяти ответственность берет на себя получатель.
В принципе делить на проходы вовсе не обязательно, можно уложиться в один присест. Так сделано лишь для простоты понимания.
Код
Первое, что сделаем — заготовим структуру TArg.
struct TArg {
// Arg type
enum {
ARGT_CHAR, ARGT_STRING
};
// Конструктор
TArg() : IntegerVal( 0 ) {
}
// Raw data
union {
int CharVal; // %c
char *StringVal; // %s
};
// Данные
int ArgType;
};
Эта структура описывает тип и значение одного аргумента. Информацию о них будем черпать, натыкаясь на спецификаторы формата в обрабатываемой строке.
Наконец, готовый вариант:
// Форматирование строки
char *StrFormat( const char *pText, ... ) throw () {
// Спецификатор формата кодируется двумя байтами
// Указатель на первый аргумент
void *pArg = ( char * ) &pText + sizeof( void * );
// Данные
TArg *Args = 0; // Аргументы
int ArgCount = 0; // Количество аргументов
int StringLen = 0; // Длинна результирующей строки
char *ResultStr = 0; // Результирующая строка
// Первый шаг: подсчитываем количество аргументов
for ( const char *pPointer = pText; *pPointer; pPointer++ ) {
if ( *pPointer == '%' ) {
if ( pPointer[1] == 's' || pPointer[1] == 'c' ) {
pPointer++; ArgCount++;
} // if
} // if
} // for
// Если есть аргументы
if ( ArgCount > 0 ) {
// Выделяем память под аргументы, обрабатываем их
Args = new TArg [ArgCount];
int ArgIndex = 0;
for ( const char *pPointer = pText; *pPointer; pPointer++ ) {
if ( *pPointer == '%' ) {
switch ( pPointer[1] ) {
case 's' :
Args[ArgIndex].ArgType = TArg::ARGT_STRING;
Args[ArgIndex].StringVal = *( ( char ** ) pArg );
// Инкремент указателя
pArg = ( char * ) pArg + sizeof( char * );
pPointer++; ArgIndex++;
break;
case 'c' :
Args[ArgIndex].ArgType = TArg::ARGT_CHAR;
Args[ArgIndex].CharVal = *( ( char * ) pArg );
// Инкремент указателя
pArg = ( char * ) pArg + sizeof( int );
pPointer++; ArgIndex++;
break;
default : break;
} // switch
} // if
} // for
// Подсчитываем, сколько нам потребуется выделить памяти под строку
StringLen = StrLength( pText ) - ( ArgCount * 2 ) + 1;
for ( int i = 0; i < ArgCount; i++ ) {
switch( Args[i].ArgType ) {
case TArg::ARGT_CHAR : StringLen++; break;
case TArg::ARGT_STRING : StringLen += StrLength( Args[i].StringVal ); break;
} // switch
} // for
ResultStr = new char [StringLen];
// А теперь - копируем
ArgIndex = 0;
int i = 0;
for ( const char *pPointer = pText; *pPointer; pPointer++ ) {
if ( *pPointer == '%' ) {
int n = 0;
switch ( pPointer[1] ) {
case 's' :
while ( *( Args[ArgIndex].StringVal ) ) {
ResultStr[i++] = *( Args[ArgIndex].StringVal );
Args[ArgIndex].StringVal++;
}
pPointer++; ArgIndex++;
break;
case 'c' :
ResultStr[i++] = Args[ArgIndex].CharVal;
pPointer++; ArgIndex++;
break;
default : ResultStr[i++] = *pPointer; break;
} // switch
} else ResultStr[i++] = *pPointer;
} // for
ResultStr[StringLen] = '\0';
delete [] Args;
} // if
// Если аргументов нет, просто клонируем строку
else ResultStr = StrClone( pText );
// Завершаем процедуру
return ResultStr;
}
Финал
Приведенную функцию еще оптимизировать и оптимизировать — на ваше усмотрение.
Спасибо за внимание.