一、字符串和普通类型的转化
在实际的开发中,可能大家都经常遇到一个问题,那就是把字符串转化成基础类型或者逆向行为。这种情况下,C/C++的相对于其它语言的劣势一览无余。要么,看上去转化方式比较简单,但适应性低,对异常处理的不好;要么,使用复杂,不容易掌握。
同时,在c++中还存在着大量的不同平台定义的不同的字符串类型,特别是一些Unicode字符串的定义,在早期更是五花八门。本文重点是分析介绍标准库的std:string与其它基础类型的转化,Unicode相关字符串的转化以后再分析。
二、基础类型转字符串方法
1、基础的入门方法
这个比较常见的是C库函数提供的几个转换函数:
itoa():将整型值转换为字符串
ltoa():将长整型值转换为字符串
ultoa():将无符号长整型值转换为字符串
gcvt():将浮点型数转换为字符串,取四舍五入
ecvt():将双精度浮点型值转换为字符串,转换结果中不包含十进制小数点
fcvt():指定位数为转换精度,其余同ecvt()
在c++11后,STD提供了一个std::to_string(val)的方法。需要注意,itoa函数并不是标准C函数,如果需要跨平台时,需要自己处理相关的事项。
看下面的小例子:
#include <iostream>
#include <charconv>
#include <stdlib.h>
#include <string>
//#include <strstream> //c++17前
#include <sstream> //c++17
#include <bitset>
#include <stdio.h>
void baseTostr()
{
std::cout << "基础类型转字符串..." << std::endl;
int num = 123;
double db = 1.233;
int dec;
int sign;
char buf[10] = {0};
_itoa_s(num, buf, 4,10);//vc
std::string sn(buf);
std::cout << "num to str is:"<<sn << std::endl;
errno_t err = _ecvt_s(buf,10,db,4,&dec,&sign);
std::cout << "double db to str is:" << buf << std::endl;
err = _gcvt_s(buf,10,db,4);
std::cout << "double db to str is:" << buf << std::endl;
auto sd = std::to_string(num);
auto sd1 = std::to_string(db);
std::cout << "num is:" << sd << ",db is:" << sd1 << std::endl;
std::string str = std::bitset<32>(num).to_string();
std::cout << "bitset string is:" << str << std::endl;
}
其实在C库和C++中还有一些库可能没有顾及到,大家查找类似的即可。此处的例子是在VS2022运行的,所以都针对微软的要求进行了修改,如果使用Posix,则改回标准C库的用法即可。
2、char*(char[])基础类型转字符串
这种就相对简单一些,可以直接使用string的构造函数或赋值构造函数:
void charsTostr()
{
std::cout << "char* or char[]转字符串..." << std::endl;
const char* p = "this is test!";
char buf[] = "my test!";
std::string s1 = p;
std::string s2 = buf;
std::cout << "p or buf transfer string is:"<<s1<<","<<s2 << std::endl;
std::string s3(p);
std::string s4(buf);
std::cout << "p or buf transfer string is:" << s3 << "," << s3 << std::endl;
}
3、使用流
见下文三节3中分析
4、格式化
见下文三节4中分析
三、字符串转基础类型方法
1、基础的方法
如同逆向运算一样,此处C/c++均提供了API,在c++中为:
stoi(s, p, b),stol(s, p, b),stod(s, p, b),stof(s, p, b),stold(s, p, b),stoul(s, p, b), stoll(s, p, b), stoull(s, p, b)等。看名字基本可以知道是字符串与整形,长整形等的转换。
C中提供的API: atof(),atoi(),atol(),strtod(),strtol(),strtoul()。同样,很容易理解,不做过多的解释。
void strTobase()
{
std::cout << "字符串转基础类型..." << std::endl;
std::string snum = "345678";
int num = atoi(snum.data());
std::string sfloat = "3.333";
double df = atof(sfloat.data());
std::cout << "num is:" << num <<",double df is:"<<df<< std::endl;
num = stoi(snum);
df = stod(sfloat);
std::cout << "num is:" << num << ",double df is:" << df << std::endl;
//C
df = strtod(sfloat.data(),nullptr);
std::cout << "c str to double:" << df << std::endl;
std::string strd = "3.333 test";
char* p = nullptr;
df = strtod(strd.data(),&p);
std::cout << "c str to double:" << df <<",other:" << p << std::endl;
}
2、std::string转char*(char[])
在c++中,经常性的要进行一些传统的C类型字符串和c++字符串的转换,即char*(char[])和string的转换:
void strTochars()
{
std::cout << "字符串转char* or char[]..." << std::endl;
std::string str = "test is 123!";
const char* pstr = str.c_str();
char* pstr1 = str.data();
std::cout << "const char * str is:" << pstr<<",char *str is:"<<pstr1 << std::endl;
char buf[30] = {0};
str.copy(buf,std::string::npos);
std::cout << "copy value is:" << buf << std::endl;
}
这里需要注意的是,一般来说,std::string转char[]都有一个拷贝的过程,而不是像char*一样直接转换。
说明:data()方法返回的数组和c_str()返回的数组区别在在于前者不会自动在末尾增加’\0’。
3、流的方法
流的方法在c++中处理有一个非常明显的好处,就是安全,不会出现有些问题字符转化出错导致的程序异常:
template<class T>
std::string base2str(const T val)
{
std::string s;
// std::stringstream ss;//C++17 pre
std::stringstream ss;
ss << val;
ss >> s;
return s;
}
int str2int(const std::string& s)
{
int val;
std::stringstream ss;
ss << s;
ss >> val;
return val;
}
double str2d(const std::string& s)
{
double val;
std::stringstream ss;
ss << s;
ss >> val;
return val;
}
void streamTransfer()
{
std::cout << "字符串与其它类型的流式转换..." << std::endl;
int num = 100;
double d = 3.33;
float f = 6.1f;
std::cout << "base to str transfer value is:" << base2str(num) << "," << base2str(d) << "," << base2str(f) << std::endl;
std::string snum = "123456";
std::string sdouble = "123.456";
std::cout << "snum to num is:" << str2int(snum) << "," << str2d(sdouble) << std::endl;
std::ostringstream nstream;
nstream << d;
std::string ns = nstream.str();
std::cout << "ostringstream to str is:"<<ns << std::endl;
std::istringstream istream;
istream.str(ns);
double d1 = 0.0;
istream >> d1;
std::cout << "to int is:"<<d1 << std::endl;
}
这里只是举了几个例子,可以按照这些方法,把所有的基本数据类型的转换都写一遍。
4、格式化输出
除了传统的sscanf(),snsprintf()等函数,在c++17中提供了from_chars()等函数:
void formatTransfer()
{
std::cout << "字符串与其它类型的格式化转换..." << std::endl;
//sscanf snsprintf
std::string snum = "36";
std::string sfloat = "6.89";
std::string sTime = "22:30";
std::string sDate = "20240511";
int num;
sscanf_s(snum.c_str(), "%d", &num);
float f;
sscanf_s(sfloat.c_str(), "%f", &f);
int hours, minutes;
sscanf_s(sTime.c_str(), "%d:%d", &hours, &minutes);
std::cout << "sscanf to base is:" << num << "," << f <<","<<hours << ","<<minutes << std::endl;
int year, month, day;
sscanf_s(sDate.c_str(), "%04d%02d%02d", &year, &month, &day);
std::cout << "cur date is:" << year << "," << month << "," << day << std::endl;
std::string sHex = "1dbc36";
int y, m, d;
sscanf_s(sHex.c_str(), "%02x%02x%02x", &y, &m, &d);
std::cout << "cur HEX is:"<<std::hex << y << "," << m << "," << d << std::endl;
num = 0x126b3c5d;
char sformat[30] = {0};
snprintf(sformat,9,"%02x",num);
std::cout << "cur hex is:" << sformat << std::endl;
//c++17
const std::string str{ "123456789" };
int value = 0;
const auto r0 = std::from_chars(str.data(), str.data() + 4, value);
if (r0.ec == std::errc())
{
std::cout << value << ", step " << r0.ptr - str.data() << std::endl;
}
else if (r0.ec == std::errc::invalid_argument)
{
std::cout << "invalid pars,transfer err!" << std::endl;
}
const std::string str1 = std::string("123.36");
double val = 0;
const auto format = std::chars_format::general;
auto r1 = std::from_chars(str1.data(), str1.data() + str1.size(), val, format);
std::cout << "val is:" << val << std::endl;
std::string str2 = std::string("abcd");
const int v = 1234;
auto r2 = std::to_chars(str2.data(), str2.data() + str2.size(), v);
if (r2.ec == std::errc())
{
std::cout << "str2 is: " << str2 << std::endl;
}
else
{
std::cout << str2 << ", err " << r2.ptr - str2.data() << " characters " << std::endl;
}
}
有些输出函数如std::cout也有一些格式控制,但太简单,这里就不再专门说明。
格式的转换其实在某些场景下特别有用,比如在图像处理或者网络数据分析中,有时候需要DUMP下相关数据进行对比,这时候儿这种十六进制的处理就非常有优势。
四、例程
在上面的基础上,增加调用函数即可形成一个完整的测试例程:
int main()
{
std::cout << "测试开始..." << std::endl;
strTobase();
baseTostr();
strTochars();
charsTostr();
streamTransfer();
formatTransfer();
std::cout << "测试完成..." << std::endl;
return 0;
}
使用上面的例子,特别是一些传统的C的接口,一定注意异常问题,比如在itoa中,如果不给一个数字字符,那么程序就异常了。而如流等的转换,包括c++17后的方法都可以提供一些安全的机制,保证转换错误也不会出现异常。整体的原则是尽量向着C++标准库靠拢。
五、总结
上面只是把常见的一些转化分析说明了一下,其实在不同的环境下,还有很多方法可以进行转换,比如使用字符一个个的处理来进行自定义,使用一些特定的函数(如各种库或框架的函数)等等。大家可以根据自己的实际情况来进行应用,不必拘泥于某种实现。但是需要引起注意的是,简单的方法往往不普遍适用,导致异常;普遍的方法往往不方便。鱼和熊掌不可得兼,请大家根据自己的场景来决定应用的方式。
unicode编码相关等以后有机会再总结分析!