65.9K
CodeProject 正在变化。 阅读更多。
Home

C++ 数字到字符串和字符串到数字的转换例程

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.70/5 (30投票s)

2014年10月24日

CPOL

4分钟阅读

viewsIcon

57837

downloadIcon

895

C++ 数字到字符串和字符串到数字的转换函数

 

引言

通常,数字需要以一种易于用户阅读的方式格式化,例如分隔千位或表示分数。反之,表示分隔千位或分数的字符串也需要转换为数值。

已提供以下转换功能

  • 数字转换为千位分隔的字符串
  • 数字转换为分数字符串
  • 数字转换为具有特定精度的字符串
  • 包含分数的字符串转换为数字
  • 包含千位分隔数字的字符串转换为数字

背景

C 语言库提供了转换函数,如 atofatolltoa ftoa。编写类似如下的代码

double d = atof("90,000.9876");

会使 d 等于 90。它会截断第一个非数字字符之后的所有内容。

对于核心函数遇到 n-illion 次调用来说,这是一种可以接受的行为。

在我过去和现在的项目中,这种转换的需求非常多。我在期货交易行业工作,那里大多数产品都以分数形式报价。因此,我决定将其封装成 C++ 例程。

Using the Code

要使用转换例程,您必须包含:

#include "conversion.hpp"

所有例程都位于名为 convert 的命名空间中。

主接口

template <typename T> inline T numeric_cast(const char* val);

template <typename T> inline T numeric_cast(const wchar_t* val);

template<typename T>
inline std::string string_cast(const T val, std::streamsize prec=5, EFmt format=none, int denom=0);

template<typename T>
inline std::wstring wstring_cast(const T val, std::streamsize prec=5, EFmt format=none, int denom=0);

它是如何工作的

将字符串转换为数字

上述接口为客户端应用程序与实现细节提供了分离层。让我们看一下该函数的实现细节。

template <typename T> 
inline T numeric_cast(const char* val)
{
  return detail::numeric_cast<T>(val);
}

正如您将看到的,此函数将调用转发到 detail 命名空间内的同名函数。这种约定借鉴了 boost 库的实现方式。detail 命名空间通常包含实现细节,因此得名。numeric_cast 函数的代码如下:

template <typename T> 
      inline T numeric_cast(const char* val)
      {
         T r = T();
         if(strlen(val) == 0)
            return r; // Do not bother

         std::stringstream ss;
         ss << detail::prepare(val);
         ss >> r;
         return r;
      }

在此函数中首先发生的是类型 T 的默认赋值。通常,编译器会将此值设置为 0。例如,编写

T i = T();

等同于

int i = int();

在大多数编译器实现中等同于

int i = 0; 

之后,会检查 string 的长度,如果 string 的长度为 0,则将默认值返回给调用函数。在这种情况下,就是 0

如果长度大于 0,则会实例化 std::stringstream 类,并调用 detail::prepare 函数。这个函数是实现将 const char* 转换为 std::string 的核心,然后将其转移到 std::stringstream,最后再从 std::stringstream 转移到类型 T

以下是 prepare 函数的核心代码

template <typename T>
inline std::string prepare(const char* val)
{
   check_valid(val);
   std::string s;
   std::string sVal = val;
   size_t pos = sVal.find('/');
   std::locale loc (std::locale::empty(), std::locale::classic(), std::locale::numeric);
   if(pos == std::string::npos)
   {
      for(size_t i = 0; i < strlen(val); i++)
      {
         char c = val[i];
         if(std::isdigit(c, loc) || c == '.' || c == '-')
            s += c;
      }
   }
   else
   {
      std::stringstream ss;
      ss.setf(std::ios::fixed, std::ios::floatfield);
      std::string sWhole, sNom, sDenom;
      size_t nWhole = sVal.find(' ');
      T result = T();

      if(nWhole == std::string::npos)
      {
         sNom = sVal.substr(0, pos);
         sDenom = sVal.substr(pos+1, sVal.length()-1);
         T nom = (T)atof(sNom.c_str());
         T denom = (T)atof(sDenom.c_str());
         if(denom != 0)
            result = nom / denom;
      }
      else
      {
         sWhole = sVal.substr(0, nWhole);
         sNom = sVal.substr(nWhole, pos-nWhole);
         sDenom = sVal.substr(pos+1, sVal.length()-1);
         T whole = atof(sWhole.c_str());
         T nom = atof(sNom.c_str());
         T denom = atof(sDenom.c_str());
         bool bNegative = false;
         if(whole < 0)
         {
            whole = fabs(whole);
            bNegative = true;
         }
         if(denom != 0)
            result = nom / denom + whole;

         if(bNegative)
            result *= -1.0;
      }
      ss << result;
      s = ss.str();
   }
   return s;
}

首先,会调用 check_valid 函数。

inline void check_valid(const char* val)
{
   std::locale loc (std::locale::empty(), std::locale::classic(), std::locale::numeric);
   for(size_t i = 0; i < strlen(val); i++)
   {
      char c = val[i];
      if(std::isalpha(c, loc))
         throw std::invalid_argument("alpha character found in numeric_cast");
   }
}

此函数会遍历 string 中的每个 char,并验证它不是字母字符。如果遇到字母字符,则会抛出 std::invalid_argument 异常。

如果在传入的 string 中未找到任何字母字符,则会搜索斜杠 ‘/’ 的位置。这是为了处理包含分数数字的字符串,如 “4 2/4”。如果未找到斜杠,则会将字符附加到 string,并去除除数字、负号 ‘-’ 和小数点 ‘.’ 之外的所有内容。如果不是分数,则返回附加后的 string 结果。

如果找到了斜杠 ‘/’,则该例程会变得更有趣。通常的分数包含三个部分:

  • 整数部分
  • 分子
  • 分母

这在数学上可以表示为:

 "4 2/4" = 4 + 2/4 = 4 + 0.5 = 4.5

首先,会搜索空格字符。如果找到空格,则将 string 分割为整数、分子和分母部分。如果缺少空格,则将 string 分割为仅分子和分母部分。然后将每个部分转换为 typename T 数字。进行负数判断,如果数字是负数,则保存负数标志。执行除法和加法运算,如果负数标志为 true,则最终的 typename T 精确结果乘以 -1.0。剩余的数字将被转移到 std::stringstream 并作为 std::string 返回。

最后,返回的 std::string 被转移到 std::stringstream,然后从 std::stringstream 转移到 typename T

如果您已经了解了 const char* 版本的工作原理,那么您也了解了 const wchar_t* 版本的工作原理。

将数字转换为字符串

template<typename T>
inline std::string string_cast(const T val, std::streamsize prec=5, EFmt format=none, int denom=0)
{
   return detail::string_cast<T>(val, prec, format, denom);
}
enum EFmt
{
   none,
   thou_sep,
   fraction,
};
  • 第一个参数是一个数字
  • 第二个是小数精度
  • 枚举格式
  • 分母(仅用于分数)

string_cast 函数调用 detail::string_cast

template <typename T>
inline std::string string_cast(const T& val, std::streamsize prec, EFmt format, int denom)
{
   std::string rVal;
   std::stringstream ss;
   ss.setf(std::ios::fixed, std::ios::floatfield);
   ss.precision(prec);
   switch(format)
   {
   case thou_sep:
      ss << val;
      rVal = ss.str();
      to_thou_sep(rVal);
      break;
   case fraction:
      to_fract(val, rVal, denom);
      break;
   default:
      ss << val;
      rVal = ss.str();
      break;
   }

   return rVal;
}

此函数会分配 std::stringstream 并将请求的流精度设置为请求的级别。使用 switch 语句处理格式枚举。

首先,函数的返回类型是 const char*,一个指针。因此,内部返回值被声明为静态的,换句话说,函数内的 static 变量是一个全局变量,仅在该函数内部可见。由于返回类型是指针,通过指针返回任何局部变量都会导致其在函数范围结束时失效。

这就完成了对内部 conversion::detail 的检查。

如果您发现有什么遗漏,请告诉我。

如有其他疑问,请参阅 conversions.hpp 文件。

如何使用

字符串到双精度浮点数转换

using namespace convert;
std::string s = "1000000.25";
std::wstring w = L"123456789";
double dS = numeric_cast<double>(s.c_str());
double dW = numeric_cast<double>(w.c_str());

双精度浮点数到千位分隔字符串

// Precision 3
std::string sThou = string_cast<double>(1000000, 3, convert::thou_sep);
// Precision 0
std::wstring wThou = wstring_cast<double>(123456, 0, connvert::thou_sep);

双精度浮点数到分数字符串

std::string s4th = string_cast<double>(90.75, 0, convert::fraction, 4);
std::string s8th = string_cast<double>(90.75, 0, convert::fraction, 8);
std::string s32nd = string_cast<double>(90.75, 0, convert::fraction, 32);
//etc

千位分隔数字字符串到数字

double dFromThouSep = numeric_cast<double>("1,000,000");

分数字符串到数字

double dFromFract = numeric_cast<double>("15 4/8");

非整数分子时的错误处理

try
   {
      double dFractionTest = 90.75;
      sFract = string_cast<double>(dFractionTest, 0, convert::fraction, 7);
      cout << "double to string fraction 7th" << endl;
    cout << sFract << endl;
   }
   catch (std::runtime_error& e)
   {
      cout << "Failed to convert 90.75 to 7th. Error: " << e.what() << endl;
   }

尝试将 90.75 转换为第 7 个分数分母会失败,因为 0.75 的结果分子是 5.25,不是整数。抛出 std::runtime_error。

 

历史

  • 2014/10/23:初始文章
  • 2015/11/27:更新代码以实现线程安全
  • 2015/11/27:当转换为分数时的分子不是整数时抛出异常
© . All rights reserved.