Pull to refresh

Велосипедостроение: sprintf своими руками

Введение

Занимаясь разработкой игрового движка, мне из некоторых соображений понадобилось написать набор функций для работы со строками, в том числе аналог библиотечной 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;
}


Финал

Приведенную функцию еще оптимизировать и оптимизировать — на ваше усмотрение.
Спасибо за внимание.
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.