C++ 数字到字符串和字符串到数字的转换例程
C++ 数字到字符串和字符串到数字的转换函数
引言
通常,数字需要以一种易于用户阅读的方式格式化,例如分隔千位或表示分数。反之,表示分隔千位或分数的字符串也需要转换为数值。
已提供以下转换功能
- 数字转换为千位分隔的字符串
- 数字转换为分数字符串
- 数字转换为具有特定精度的字符串
- 包含分数的字符串转换为数字
- 包含千位分隔数字的字符串转换为数字
背景
C 语言库提供了转换函数,如 atof
、atol
和 ltoa 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:当转换为分数时的分子不是整数时抛出异常