Алгоритмы преобразования строки в число и обратно

Выполнить преобразование строки в число можно многими способами - выбор конкретного зависит от ваших целей на момент написания кода. Есть штатные способы - ряд библиотечных функций, есть более изощренные, есть совсем уж извращенные годные разве что для экзерсисов в области программирования. Начну с самых простых.
Первый, и, наверное, самый распространенный, но далеко не самый лучший - использование штатных библиотечных функций atoi, atof, atol. Эти функции входит в стандартную библиотеку языка и присутствует в любом компиляторе. Их объявления выглядит так:

int atoi(const char* str)
long atol(const char* str)
double atof(const char* str)

На вход они принимают указатель на строку, завершенную нулем, а возвращают - число, которое этой строкой описывается. atoi и atol воспринимают следующий формат числа:

[пробелы][знак]цифры
а atof, соответственно:
[пробелы][знак][цифры][.цифры][{d | D | e | E }[знак]цифры]

Здесь пробелы - любой из знаков пробела, табуляции (\t), вертикальной табуляции (\v) - они игнорируются. Знак - символ '+' или '-'. Если не указан, то считается, что число положительное. Цифры - символы от '0' до '9'. Для числа с плавающей точкой, если не указаны цифры до знака '.', то должна быть указана хотя бы одна цифра после него. После дробной части может быть указана экспонента, следующая за одним из символов-префиксов экспоненты.
Основной недостаток этих функций заключается в том, что они никак не сигнализируют об ошибке, если таковая произошла в процессе разбора переданной строки. Под ошибкой я понимаю невозможность корректно разобрать переданный набор символов - несоответствие его формату или по иным причинам.
Эту проблему решает следующий набор библиотечных функций, также включенных в стандартную библиотеку:

long strtol(const char* str, char** end_ptr, int radix)
unsigned long strtoul(const char* str, char** end_ptr, int radix)
double strtod(const char* str, char** end_ptr)

Эти функции имеют следующие отличия от предыдущей группы:
  • Через параметр end_ptr они возвращают указатель на первый символ, который не может быть интерпретирован как часть числа.
  • Контролируют переполнение и, если таковое произошло, сигнализируют об этом выставлением значения переменной errno в ERANGE, а также возвращают, соответственно, LONG_MAX/LONG_MIN, ULONG_MAX/ULONG_MIN и +/-HUGE_VAL в зависимости от знака числа в переданной строке.
  • strtod использует информацию о текущих установленных (через setlocale) региональных настройках, таким образом может корректно интерпретировать числа с символом ',' в качестве разделителя целой и дробной части.
  • Для функций strtol и strtoul можно указать основание системы счисления. При этом, если в качестве основания передан 0, то основание определяется автоматически по первым символам числа. Если это символ '0', а сразу за ним идет цифра - то основание принимается равным 8. Если первая цифра '0', а за ней идет символ 'x' или 'X', то основание принимается равным 16. В остальных случаях основание принимается равным 10. В качестве цифр в этом случае можно использовать символы '0' - '9' и 'A' - 'Z' или 'a' - 'z', а основание может принимать значения от 2 до 36.
  • Если варианты этих функций для преобразования чисел, описанных unicode-строками. Они, соответственно, носят названия wcstol wcstoul и wcstod.
Типичное использование этих функций такое:

char* end_ptr;
long val = strtol(str, &end_ptr, 10);
if (*end_ptr)
{
// Сигнализируем об ошибке в строке
}
if ((val == LONG_MAX || val == LONG_MIN) && errno == ERANGE)
{
// Сигнализируем о переполнении
}
// Продолжаем штатную работу.

Как можно увидеть, используя эти функции можно получить гораздо больший контроль над преобразованием из строки в число. Для различных преобразований рекомендую пользоваться именно ими.
Говоря о стандартных библиотечных функциях нельзя не упомянуть такую функцию как scanf и ее разновидности - sscanf, fscanf и т. п:

int scanf(const char* format, ...)
int sscanf(const char* buff, const char* format, ...)
int fscanf(FILE* file, const char* format, ...)

и т. п.
Эту функцию имеет смысл использовать только в случае получения числа от пользователя с консоли (stdin) или из файла. Надо отметить, что функция весьма тяжеловесна (по объему линкуемого к исполняемому модулю библиотечного кода), и далеко не так быстра, как предыдущие, т. к. для преобразования необходимо разобрать форматную строку и соответствующим образом ее проинтерпретировать.
Аналогично можно использовать операторы потокового ввода ('>>'). В случае написания программ на C++ этот вариант гораздо предпочтительней, чем использования метода scanf, т. к. обеспечивает гораздо больший контроль за типами на этапе компиляции. Ожидаемый формат числа можно указать с помощью флага формата:
dec - целое в десятичном формате;
hex - целое в шестнадцатеричном формате;
oct - целое в восьмеричном формате;
scientific - число с плавающей точкой в экспоненциальном формате;
fixed - число с плавающей точкой в фиксированном формате.
При этом форматы для целых чисел и чисел с плавающей точкой устанавливаются независимо друг от друга.
В случае получения результатов ввода пользователя из строки редактирования (EditBox, CEdit) имеет смысл пользоваться методом GetDlgItemInt:

UINT GetDlgItemInt(HWND hDlg, int itemId, BOOL* pTranslated, BOOL signed)
UNIT CWnd::GetDlgItemInt(int itemId, BOOL* pTranslated, BOOL signed)

Первым параметром в API-вызов передается хэндл диалогового окна, которому принадлежит строка редактирования. Параметр itemId задает идентификатор строки ввода, через pTranslated возвращается признак того, что введенная пользователем строка успешно проинтерпретирована как целое число. Параметром signed задается необходимость получения в качестве результата функции знакового числа.
Для преобразования экземпляра класса CString в число можно воспользоваться любым из предложенных выше методов, т. к. CString без труда преобразуется в указатель на null-terminated-строку. Специальных методов для этого класса разработчиками библиотеки не предусмотрено.
В VCL ситуация чуть лучше. Для класса AnsiString из этой библиотеки определены следующие методы, позволяющие получить число из его строкового представления:
ToInt - простое преобразование строки в целое число;
ToDouble - преобразует строку в число с плавающей точкой. Формат разделительных символов читается из региональных настроек системы.
Оба метода (в случае несоответствие строки формату числа) выбрасывают исключение. Также стоит обратить внимание на метод ToIntDef, который (в случае неудачного преобразования) возвращает значение по умолчанию, переданное ему в качестве параметра.
Вот основные библиотечные и API-функции, которые можно использовать для преобразования строки в число.

Преобразование числа в строку

К сожалению, стандартной специализированной функции (описанной в стандарте языка) для обратного преобразования (числа в строку) не существует. Но это не значит, что таких функций нет в RTL компиляторов. Для преобразования целого числа в строку можно воспользоваться следующими функциями:

char* _itoa(int value, char* string, int radix);
wchar_t* _itow(int value, wchar_t* string, int radix);
char* _ltoa(long value, char* string, int radix);
wchar_t* _ltow(long value, wchar_t* string, int radix);
char* _ultoa(unsigned long value, char* string, int radix);
wchar_t* _ultow(unsigned long value, wchar_t* string, int radix);

эти функции входят в библиотеку компиляторов от Microsoft.
Для компиляторов фирмы Borland можно использовать следующие функции:

char* itoa(int value, char* string, int radix);
wchar_t* _itow(int value, wchar_t* string, int radix);
char* ltoa(long value, char* string, int radix);
char* _ltoa(long value, char* string, int radix);
wchar_t* _ltow(long value, wchar_t* string, int radix);
char* ultoa(unsigned long value, char* string, intradix);
wchar_t* _ultow(unsigned long value, wchar_t* string, int radix);

Как можно видеть - в компиляторах семейства Microsoft и Borland имена функций отличаются не сильно и формируются по следующему принципу:
[_]исходный_типtoформат_строки. Исходные типы представлены следующими аббревиатурами:
i - int;
l - long;
ul - unsigned long;
i64 - int64;
ui64 - unsigned int 64;
формат строки, соответственно:
a - однобайтный char (ANSI);
w - wide-char (Unicode).
Параметры, принимаемые на вход, имеют следующий смысл. Первый параметр (value) - это значение, которое необходимо преобразовать в строку. Второй (string) - буфер, в который будет помещен результат преобразования. А третий (radix) - основание системы счисления, в которой будет представлено число.
Сказав выше, что нет стандартных функций преобразования числа в строку, я немного лукавил, так как имел в виду именно специализированные функции - аналоги atoi и strtol. К неспециализированным стандартным функциям, выполняющим необходимое преобразование, можно отнести функцию sprintf:

int sprintf(char* buffer, const char* format [, argument] ... );

выполняющую форматированный вывод в заданный буфер, а также другие функции из этого семейства. Для необходимого нам преобразования необходимо воспользоваться форматной строкой "%d". При этом будет выполнено стандартное преобразование в десятичное представление числа, аналогичное предыдущим функциям. Формат полученного числа определяется тэгом типа. Существуют следующие тэги типов:
d (или i) - знаковое целое в десятичном формате;
u - беззнаковое целое в десятичном формате;
o - беззнаковое целое в восьмеричном формате;
x (или X) - беззнаковое целое в шестнадцатеричном формате.
Можно использовать также специальные спецификаторы тэгов типа (указываются непосредственно перед тэгом) для того, чтобы указать специальный формат, в котором передано число:
l - передано длинное целое (signed/unsigned long);
h - передано короткое целое (signed/unsigned short);
l64 (для Microsoft) или L (для Borland) - передано 64-битное целое (signed/unsigned __int64).
Вообще говоря, при использовании спецификатора типа h необходимо понимать, что при передаче в функцию printf переменных типа short (по стандарту) продвигаются до типа int. Таким образом, этот спецификатор просто дает указание функции форматирование игнорировать старшие разряды полученного целого параметра.
Функции форматного вывода хороши тем, что им можно явно указать формат, в котором мы хотим получить результат. В частности, форматная строка "%08x" преобразует переданное число в строку, содержащую шестнадцатеричное представление числа шириной 8 символов, при этом, при необходимости, строка будет дополнена слева незначащими нулями до 8 символов. Также достоинством этих функций (перед предыдущей группой) является то, что строка преобразуется в соответствии с установленными для программы (с помощью вызова функции setlocale) региональными настройками (locales).
К основным недостаткам методов форматного вывода можно отнести их низкую скорость работы (на порядок медленней их специализированных аналогов), невозможность преобразования в системы счисления, отличные от восьмеричной, десятичной, и шестнадцатеричной, а также отсутствия контроля за типами и размером переданного буфера, что может приводить к трудновыявляемым ошибкам.
Для перевода чисел с плавающей точкой предназначены следующие методы:

char* _ecvt(double value, int count, int* dec, int* sign);
char* _fcvt(double value, int count, int* dec, int* sign);
char* _gcvt(double value, int digits, char* buffer);

- для Microsoft-компиляторов и

char* ecvt(double value, int count, int* dec, int* sign);
char* fcvt(double value, int count, int* dec, int* sign);
char* gcvt(double value, int digits, char* buffer);

для компиляторов от фирмы Borland.
Назначение функций следующее:
Функция ecvt конвертирует число (value) в заданное (count) количество символов, не выполняя при этом никакого форматирования. Через параметр dec возвращается позиция, с которой начинается дробная часть (позиция десятичной точки), начиная с первого символа строки, а через параметр sign - признак того, что число имеет знак. Тут необходимо сделать ряд следующих замечаний:
  1. Если строковое представление числа уже, чем необходимое количество символов, то число справа дополняется нулями.
  2. сли строковое представление числа шире, чем необходимое количество символов, то возвращаемая через dec позиция находится правее завершающего строку нуля или левее первого символа строки. Выбор знака dec зависит от знака десятичной экспоненты числа. Например, для value= 3.5678e20 и count=6 функция вернет строку "356780", а dec будет равно 21. А для значения 3.5678e-20 и 6, будет возвращено, соответственно, "356780" и -19.
  3. Преобразование производится во внутренний статический буфер, разделяемый с функцией fcvt, по этому необходимо быть осторожным при использовании функции в многопоточной среде.
Функция fcvt отличается от ecvt только тем, что параметр count задает не общее число символов, а число символов после десятичной точки (точность представление числа). При этом строковое представление (по необходимости) дополняется справа нулями до получения нужной ширины. Параметры dec и sign ведут себя аналогичным (как и для ecvt) образом. Для приведенных выше примеров возвращенные значения будут "356780000000000000000000000" и 21, а также "" и -19 соответственно. В последнем случае возвращенная пустая строка означает, что строковое представление содержит только нули.
Функция gcvt преобразует заданное число (value) в привычное строковое представление и помещает результат в переданный буфер (buffer). При этом в буфер будет помещено не более digits цифр. Если число может быть представлено таким количеством цифр, то оно будет преобразовано в десятичный (фиксированный) формат, иначе будет выполнено преобразование в экспоненциальный формат, и заданное количество цифр будет содержать мантисса числа. Полученное строковое представление может быть дополнено справа нулями для получения необходимого количества разрядов.
При использовании этой функции необходимо помнить следующее: буфер должен быть достаточно большим, чтобы принять digits цифр, завершающий ноль, а также знак, десятичную точку и экспоненту. Никакого контроля не производится, и граница буфера может быть нарушена. При формировании строки региональные настройки учитываются.
Для вывода чисел с плавающей точкой у функций семейства printf есть следующие тэги типов:
е (или Е) (exponent) - преобразует число в экспоненциальное представление;
f (fixed) - преобразует число в десятичное представление основываясь на дополнительных форматных параметрах (ширина и точность);
g (или G) (general) - преобразует число либо в экспоненциальное, либо в десятичное представление в зависимости от того, какое представление будет короче.
Так же, как и для целых чисел, можно указать дополнительные спецификаторы:
L - передано число в формате long double.
Необходимо заметить, что при передачи переменных типа float производится их продвижение до типа double.
Дополнительно можно указать общее количество значащих цифр в строковом представлении (ширну), а также количество цифр в дробной части (точность) в следующем виде:
%<ширина>.<точность><тэг_типа>.
Подробнее об этих форматах можно прочитать в документации по форматной строке функций этого семейства, т. к. подробное описание всех возможностей функций семейства printf выходит за рамки этой статьи.
В C++ числа можно конвертировать в символьное представление с помощью операторов потокового вывода (<<). При этом для выводимых чисел можно установить желаемую точность (метод precision) и ширину (метод width). Желаемый формат устанавливается путем вывода в поток соответствующего форматного флага:
dec - целое в десятичном формате;
hex - целое в шестнадцатеричном формате;
oct - целое в восьмеричном формате;
scientific - число с плавающей точкой в экспоненциальном формате;
fixed - число с плавающей точкой в фиксированном формате.
При этом форматы для целых чисел и чисел с плавающей точкой устанавливаются независимо.
При выводе региональные настройки также учитываются, но устанавливать их нужно особым образом, и обсуждение этого вопроса выходит за рамки этой статьи.
Если говорить о возможностях библиотек MFC и VCL, то в MFC единственный способ преобразовать число в строку - это метод Format класса CString. Этот метод работает аналогично методу sprintf.
У VCL в этом плане возможностей больше. У класса AnsiString существуют следующие статические методы:
IntToHex - преобразует число в шестнадцатеричное представление.
FormatFloat - преобразует число с плавающей точкой в строку в соответствии с заданной (в виде строки) маской, что позволяет получать числа практически в любом желаемом формате.
FloatToStrF - преобразует число в строку в соответствии с несколькими самыми распространенными форматами (sffGeneral, sffExponent, sffFixed), которые соответствуют форматам функции sprintf (g, e, f). Также этот метод может добавить в строку разделители тысяч (формат sffNumber), или преобразовать в представление денежных единиц (формат sffCurrency). При этом все разделительные символы, формат числа и т. п. будут взяты из текущих региональных настроек системы.
Также существуют (но не отражены в документации) нестатические методы класса AnsiString sprintf и printf, аналогичные по возможностям соответствующим RTL-ным, но не принимающие на вход буфер, в качестве которого используется экземпляр класса AnsiString.
В WinAPI (симметрично методу GetDlgItemInt) существует метод SetDlgItemInt, выполняющий необходимые преобразования числа в строку:

UINT SetDlgItemInt(HWND hDlg, int itemId, UINT value, BOOL signed)
UNIT CWnd::GetDlgItemInt(int itemId, UINT value, BOOL signed)

Последним параметров в эти функции передается признак знаковости числа.

Вот наверное и все, что можно сказать про штатные функции преобразования. В следующей части будут рассмотрены различные варианты "самописных" функций преобразования.

А можно оптимизировать и обобщить ф-ии подобные _itow, _ltow, _ultow, _i64tow, _ui64tow и их ANSI аналоги. Причём ниже приведённая ф-ия будет работать куда быстрее чем стандартные. Например, для преобразования целого в строковый параметр UNICODE, все аналоги с префиксом _w, сначала создают буфер char фиксированного размера, затем вызывают одноимённые версии для С-строк, которые вызывают xtoa или x64toa, затем происходит вызов ф-ий преобразования для преобразования ANSI в UNICODE (mbstowcs и __mbstowcs_mt). Эта ф-ии не делает никаких косвенных вызовов, и требует меньше времени на прохождение по её телу. Прюс ещё одно отличие от стандарта, при преобразовании в 16-ричное число к строке добавляется префикс "\0x".
 
// The following constants must be used during the function call.
enum Radix {
    BINARY = 2,
    OCTOPLE = 8,
    DECIMAL = 10,
    HEXADEMICAL = 16
};

template<class Int, class Str>
void
APIENTRY
itos(Int val, Str buf, UINT size, Radix r=DECIMAL) throw()
{
    Str offset = buf;   // a pointer to a string
    Str first = offset; // a pointer to
                        // the first digit
    UINT delta_y;        // the value of a digit
    UINT count = 0;     // a number of digits
                        // in the string

    // val is negative, so let's add '-' to
    // the begin of the string.
    if (val < 0) {
        *offset++ = '-';
        val = -val;
        first = offset;
    }

    // Take special actions for hexademical and
    // octople countable systems and insert to the
    // begin of the string either the "\0x" or '0'
    // prefixes respectively.
    if (r == HEXADEMICAL) {
        *offset++ = '0';
        *offset++ = 'x';
        first = offset;
    }
    else if (r == OCTOPLE) {
        *offset++ = '0';
        first = offset;
    }

    // Read and store digits into the string.
    while (val > 0 && size-- >= 0) {
        delta_y = (Int)(val % r);
        val /= r;

        if (delta_y > 9)
            // The hexademical format.
       *offset++ = (delta_y - 10 + 'a');
        else
            // Binary, dicimal and octople formats.
            *offset++ = (delta_y + '0');
        count++;
    }

    // Points to the last digit.
    *offset-- = '\0';

    // Now our string corresponds to the integer
    // digits but it is in the reverse order. So make
    // the backward transformation of it. If it has
    // only two characters just swap them without any
    // additional actions.
    if (count == 1)
        return;
    else if (count == 2) {
        char temp = (char)*offset;
        *offset-- = *first;
        *first++ = temp;      
    }
    // For more than two characters in the string
    // reverse full string in the folowing cycle.
    else {
        delta_y = count / 2;

        while (delta_y--) {
            char temp = (char)*first;
            *first++ = *offset;
            *offset-- = temp;
        }
    }
}
}

Использование ф-ии "почти" не отличается от стандартных.

Думаю, таой способ тоже имеет право на жизнь:
#include <string>
#include <sstream>

template <typename T>
std::string toString(T val)
{
    std::ostringstream oss;
    oss<< val;
    return oss.str();
}

template<typename T> 
T fromString(const std::string& s) 
{
  std::istringstream iss(s);
  T res;
  iss >> res;
  return res;
}

// Пример использования
std::string str;
int iVal;
float fVal;

str = toString(iVal);
str = tiString(fVal);

iVal = fromString<int>(str);
fVal = fromString<float>(str);
 
« Предыдущая статья   Следующая статья »