摘要:本文總結了C/C++中的多種數據類型轉換方法,並比較了各自的優劣。給出了推薦的使用建議。
從int到char*,或者反過來從char*到int,在C/C++中到底有多少種轉換方法呢?符合標準的大概有四種。即C數據轉換函數族、sprintf/snprintf/sscanf函數族、字符串流std::stringstream、std::strsteam。不符合標準卻又廣爲使用的包括CString和boost::lexical_cast。本文只討論符合標準的轉換方法,其中std::strstream由於已經被C++標準委員爲指定爲不推薦使用的(deprecated),所以不予考慮了。下面重點討論三種標準轉換方法之間的優劣。源代碼的地址是:http://download.csdn.net/source/631475
1. Int和char*或std::string之間的轉換
C數據轉換函數族
C數據轉換函數族即包括itoa、atoi等數據類型轉換函數在內的一大批C函數,在C語言時代曾經被大量使用。源代碼如下:
int i = 10;
char szBuf[10] = "";
itoa(i, szBuf, 10);
cout<<"itoa: szBuf = "<<szBuf<<endl;
i = 0;
i = atoi(szBuf);
cout<<"atoi: i = "<<i<<endl;
使用還是比較簡單的。一個最大的問題是:沒有進行char*的越界檢查,可能會造成數組溢出。
snprintf/sscanf
sprintf是用來格式化字符串的一個C函數,sscanf則是從字符串中讀取值的一個C函數。由於Herb Sutter(Exceptional C++系列著作的作者)教導我們“永遠也不要使用sprintf”,所以這裏我們只使用snprintf。由於snprintf進入C標準較晚,所以在你的編譯器中也許只能使用非標準的_snprintf(例如我的VC6平臺)。源代碼如下:
int i = 20;
char szBuf[10] = "";
memset(szBuf, 0, sizeof(szBuf));
_snprintf(szBuf, sizeof(szBuf), "%d", i);
cout<<"_snprintf: szBuf = "<<szBuf<<endl;
i = 0;
sscanf(szBuf,"%d",&i);
cout<<"sscanf: i = "<<i<<endl;
使用很簡單,而且,似乎沒有什麼內存泄露或者數組越界。
std::stringstream
對流很熟悉的人可能會更快適應std::stringstream的解決方案:
#include <sstream>
using namespace std;
int i = 30;
string strRel;
ostringstream oss;
oss<<i;
strRel = oss.str();
cout<<"ostringstream: strRel = "<<strRel<<endl;
i = 0;
istringstream iss(strRel);
iss>>i;
cout<<"istringstream: i = "<<i<<endl;
使用較爲複雜,而且,還使用了兩個臨時變量oss和iss,這必然帶來性能上的開銷。
2. double和char*或std::string之間的轉換
C數據轉換函數族
當開始進行double和char*之間的轉換時,C數據轉換函數的缺點暴露無疑。先看源代碼:
double d = 3.1415926;
char szBuf[18] = "";
_gcvt(d, 9, szBuf);
cout<<"_gcvt: szBuf = "<<szBuf<<endl;
d = 0;
char* stopstring;
d = strtod(szBuf, &stopstring);
cout<<"strtod: d = "<<d<<endl;
首先轉換函數的名字就讓人大吃一驚,與itoa對應的不是我們想象的dtoa,而是_gcvt,而與atoi對應的是strtod。其次它們的參數很奇怪,沒有msdn是不可能明白的。其次,數組越界依然存在。至此我想我們可以拋棄這組函數了。當然,更加無奈的理由在後面。
snprintf/sscanf
snprintf/sscanf表現不錯,源代碼如下:
double d = 3.1415926;
char szBuf[18] = "";
memset(szBuf, 0, sizeof(szBuf));
_snprintf(szBuf, sizeof(szBuf), "%f", d);
cout<<"sprintf: szBuf = "<<szBuf<<endl;
sscanf(szBuf, "%f", &d);
cout<<"sscanf: d = "<<d<<endl;
很好,很強大!
std::stringstream
std::stringstream的代碼似乎沒有任何改動,除了一個int類型改成了double類型:
double d = 9.1415926;
string strRel;
ostringstream oss;
oss<<d;
strRel = oss.str();
cout<<"ostringstream: strRel = "<<strRel<<endl;
d = 0;
istringstream iss(strRel);
iss>>d;
cout<<"istringstream: d = "<<d<<endl;
寫到這裏,我似乎看到模板函數在向我招手。
3. 複雜的轉換
考慮一個經典的場景,從一個int,一個double和一個string中讀出值,然後拼湊爲一個輸出的字符串。最後,從這個字符串中再將這幾個值讀出來。
C數據轉換函數族
直接看代碼:
int iAge = 25;
float fPayment = 3.25;
string strName ="Wang";
char szBuf[100] = "";
char szTemp[100];
strcpy(szBuf,"Age= ");
itoa(iAge, szTemp, 10);
strcat(szBuf, szTemp);
strcat(szBuf," ,Payment= ");
_gcvt(fPayment, 4, szTemp);
strcat(szBuf,szTemp);
strcat(szBuf," ,Name= ");
strcat(szBuf, strName.c_str());
cout<<"szBuf = "<<szBuf<<endl;
以上代碼的表現真是慘不忍睹,費了幾鼻子的勁好歹是轉爲目標字符串了。轉換回來的代碼也沒有寫。也許有,不過那個複雜程度,我看還是算了。而且,strcpy、strcat和幾個轉換函數都是危險的API,不檢查越界的。
snprintf/sscanf
主要看看sscanf的表現:
int iAge = 25;
float fPayment = 3.25;
string strName ="Wang";
char szBuf[100] = "";
memset(szBuf, 0, sizeof(szBuf));
_snprintf(szBuf, sizeof(szBuf), "Age = %d, Payment = %f, Name = %s",iAge,fPayment,strName.c_str());
cout<<"sprintf: szBuf = "<<szBuf<<endl;
iAge = 0;
fPayment = 0.0;
memset(szTemp, 0, sizeof(szTemp));
sscanf(szBuf,"Age = %d, Payment = %f, Name = %s",&iAge,&fPayment,&szTemp);
strName = szTemp;
cout<<"sscanf: Age = "<<iAge<<",Payment="<<fPayment<<",name="<<strName<<endl;
snprintf表現還是一如既往的強大。sscanf的表現簡直就是perfect,但是要注意,sscanf的樣式字符串一定要和snprintf中的樣式字符串一模一樣,否則其後果是不可預計的。例如,我稍微改動了幾個字符,最後的strname就讀取錯誤了。
std::stringstream
std::stringstream的代碼很長很長:
int iAge = 25;
float fPayment = 3.25;
string strName ="Wang";
string strRel;
ostringstream oss;
oss<<"Age = "<<iAge<<", Payment = "<<fPayment<<", Name = "<<strName;
strRel = oss.str();
cout<<"ostringstream: strRel = "<<strRel<<endl;
iAge = 0;
fPayment = 0.0;
strName = "";
istringstream iss(strRel);
string strTemp;
iss>>strTemp>>strTemp>>iAge>>strTemp>>strTemp>>strTemp>>fPayment
>>strTemp>>strTemp>>strTemp>>strName;
cout<<"istringstream: Age = "<<iAge<<",Payment="<<fPayment<<",name="<<strName<<endl;
ostringstream的表現還是不錯的,比snprintf毫不遜色,甚至更好一點,因爲它不用記樣式符。但是看到istringstream的這幾行代碼,估計大部分人要吐血了:
string strTemp;
iss>>strTemp>>strTemp>>iAge>>strTemp>>strTemp>>strTemp>>fPayment
>>strTemp>>strTemp>>strTemp>>strName;
爲什麼中間有那麼多strTemp?因爲每當istringstream每當遇到由一個空格或者非數字字符包圍的字符串時就必須輸入到一個string中,因此例如“,”或者“=”都必須佔用一個string來輸入。總之,當字符串很複雜時,很麻煩。
4. 小結
從易用性、安全性和效率三個方面來考察以上方法。
易用性按從好到壞排列依次是:snprintf/sscanf、std::stringstream、C數據轉換函數族。
安全性按從好到壞排列依次是:std::stringstream、snprintf/sscnaf、C數據轉換函數族。
效率按從好到壞排列依次是:snprintf/sscanf、C數據轉換函數族、std::stringstream。
到此我們可以拋棄C數據轉換函數族了,接下來從其他方面來考察剩下的方法。
5. 模板函數
考慮寫一個從任何內置數據類型到字符串的轉換函數。此時只能使用std::stringstream了。
template<typename T> string to_str(T val)
{
ostringstream oss;
oss<<val;
return oss.str();
}
string strFromInt = to_str(10);
cout<<strFromInt<<endl;
string strFromDouble = to_str(3.1415926);
cout<<strFromDouble<<endl;
換句話說,也就是std::stringstream是類型安全的,而snprintf不是。
6. 精度和格式
前面的例子都忽略了精度,事實上要進行精度和格式的設置,snprintf和std::stringstream的學習時間是差不多的。這裏就不一一敘述了。
7. 再談安全性
snprintf是安全的,因爲它會檢查數組越界並阻止這種行爲。但是當目標字符數組長度小於需求時,其結果是不正確的。sscanf也是同樣,當它的樣式字符串不匹配時,其結果是未知的。
std::stringstream是更安全的。由於它採用了內存自動管理機制。無論在任何時候,只要它沒有拋出異常,其結果總是正確的。
8. 總結
在易用性和效率方面,snprintf/sscanf都比std::stringstream強,因此只要程序員能夠保證目標字符數組長度夠用時,都可以放心使用snprintf。sscanf也是同理,只要程序員保證其樣式字符串正確,其結果也總是正確的。
而std::stringstream的最大優點是具有模板親和力,可以用於泛型編程。在效率不是很重要的情況下,建議使用std::stringstream。若你是一位非常謹慎的程序員,建議你總是使用std::stringstream,因爲它是最安全的