如果按照內容來給一個標題的話,那麼這一講的內容其實是上一講的後續部分,所以都屬於C++標準流一類,但是又礙於我們這不是寫書,而是按照文章來推送,所以這算是一個新的章節,然儘管如此,這依然算是C++的流的介紹,所以,在上一章文章中我們了解了C++標準流的用法後那麼我們現在從文件流說起。
程序無非是數據的操作,最常用的莫過於數據的讀寫了,還記得我們在上一講的內容中使用自定義的流擴展了一個文件流——FileOStream,該類繼承至MyStrIobase,當然我們可以直接繼承至OStream,好吧,想想為什麼要繼承至OStream,繼承至OStream的優勢又是什麼。
我們不對FileOstream進行討論,至少大家已經知道了C++標準流背後的一些原理,所以我們這一講的內容將是站在上一講的基礎上來對fstream和stream的探索。
從fstream說起
在C++標準庫中,fstream繼承至iostream,所以iostream該有的操作他都有,fstream還具有iostream不具有的能力——文件的度讀寫。
如同上一講的內容,文件的讀寫簡單點說就是將數據發送到指定的目的地或者是從指定的地方將數據取回來,所以,我們可以這麼來解讀iostream所幹的事——將這個目的地給固定了,而fstream卻可以自由指定這個目的地——文件,對於文件我們可以使用構造函數來指定,同樣可以使用open接口來重定向:
//+---
#include <fstream>
int main(){
std::ofstream outFile("1.txt",std::ios::out);
outFile<<"Hello World"<<std::endl;
outFile.close();
outFile.open("2.txt",std::ios::out);
outFile<<"Hello World2"<<std::endl;
outFile.close();
std::ifstream inFile("1.txt",std::ios::in);
std::string str;
std::getline(inFile,str);
std::cout<<str<<std::endl;
inFile.close();
return 0;
}
//+----
從上面的代碼中,我們可以看到,我們可以通過構造函數來打開文件,同樣也可以通過提供的成員函數open來打開文件,當我們寫完數據之後我們可以使用close來關閉文件,關閉當前文件後又可以打開其他文件,ofstream用來將數據寫入文件,ifstream用來從文件中讀取,所以,有了第一章的基礎後來使用fstream是非常簡單的,當然或許我們要說說的是對於二進位文件的讀寫,對於二進位數據還記得我們上一講中說到的write函數嗎?
//+---
ostream& write(const char*,streamsize);
//+---
當時我們說這個函數可以用來處理字符串,其實它不只是能夠處理字符串,他能夠處理一切數據,為什麼這麼說呢?首先,它的第一個參數是一個char的指針,第二個參數是一個大小,而char*可以轉換為任意數據的指針,同樣任意數據都可以轉換char*,比如:
//+---
int main(){
int a = 10;
char* ch = (char*)(&a);
int d = *(int*)(ch);
std::cout<<d<<std::endl;
return 0;
}
//+---
我們將一個int的對象存儲在一個char*中然後又再取出來,數據得到很好的還原。我們再來看一些更為複雜的:
//+---
struct Test{
int a;
double b;
long long c;
};
int main(){
Test test = {10,20.0,1000LL};
char* ch = (char*)(&test);
Test test2 = *(Test*)(ch);
std::cout<<test2.a<<std::endl;
std::cout<<test.b<<std::endl;
std::cout<<test.c<<std::endl;
return 0;
}
//+----
就算是複合類型也毫無問題,那麼我們是不是明白了write這個函數對於數據的讀寫能力的強大之處了呢?所以當我們要保存或是恢復一個對象的時候可以如下操作:
//+---
struct Test{
int a;
double b;
long long c;
};
int main(){
Test test = { 10, 20.0, 1000LL };
std::ofstream outFile("1.txt", std::ios::binary | std::ios::out);
outFile.write((char*)(&test), sizeof(Test));
outFile.close();
std::ifstream inFile("1.txt", std::ios::binary | std::ios::in);
char* ch = new char[sizeof(Test)];
memset(ch, 0, sizeof(Test));
inFile.read(ch, sizeof(Test));
inFile.close();
Test test2 = *(Test*)(ch);
std::cout << test2.a << std::endl;
std::cout << test.b << std::endl;
std::cout << test.c << std::endl;
return 0;
}
//+----
我們可以將一個對象存儲在硬碟裡面需要的時候可以將他恢復出來,這就是fstream的write和read的妙用。上一講我們並沒有接觸過read,那麼read函數的原型如下:
//+---
ifsteram& read(char*, streamsize)
//+---
該函數的功能是從流中讀取指定大小的字節數據,數據填充在指定的地方——第一個參數指定的地址。
至此,使用C++讀寫文件對我們來說已經是很輕鬆的事了,那麼接下來我們來看看在C++流中我認為算是一個很高級的東西——stringstream。
stringstream,顧名思義就是字符串流,對於不少C++程式設計師來說這個這個組件可能用得比較少,或許可能很多人沒聽說過,比如就我就遇到有人不知道該流的存在,更別說用法了,因為這東西實在用得比較少,而且如果只是普通的使用C++的話stringstream是可以完全無視的。噢……既然可以被無視的東西為什麼我們這裡要說呢?而且更是將stringstream的使用來作為這一章的標題。好吧,原因很簡單,在我看來stringstream雖然不常用,但並不表示它沒用。
設想一個場景,假如有兩個函數,兩個函數需求的參數類型各不相同,但如今我們會用到這兩個函數,為了簡便操作,我們將兩個函數封裝成一個函數:
//+
void f(const std::string& str){
std::cout<<str<<std::endl;
}
void g(int a){
std::cout<<a<<std::endl;
}
template<class T>
void fun(T val){
//
// 根據val的類型不同來調用不同的函數
// 如果是int調用g
// 如果是字符串調用f
// 否則不執行
//
}
//+-
現在我們拿到的f和g是由不同的人提供給我們的函數,我們要將這兩個功能應用到我們的程序之中,這時為更方便我們可以對其進行封裝得到我們的fun函數,當我們使用的時候就不需要關心我們到底調用的是哪一個函數,它會根據我們的參數類型而選擇適合的函數進行調用。這裡想要優雅的實現我們的fun說簡單也不簡單,說難也不難,而這正是這一講要講的東西。
將任意非字符串對象轉換為字符串有多少種方法呢?常用的可能就是sprintf,這是C語言提供的庫函數,如果不考慮跨平臺可能用得最多的應該就是itoa,ltoa,ultoa...等一系列轉換函數,儘管這些方法雖然都很好用,但我還是覺得stringstream可以比他們更加優雅,而且stringstream 可以做的事情遠比想想的要多,下面是 stringstream 的基本使用:
//+--
void f(const std::string& str);
void fun(int i){
//
// 將i轉換為string然後調用f
//
std::stringstream os;
os<<i;
f(os.str());
}
void fun2(const char* ch){
//
// 將ch 轉換為 int然後調用fun
//
std::stringstream is;
is<<ch;
int i;
is>>i;
fun(i);
}
//+
我們將數據流到stringstream中,然後使用成員函數str提取出來就是字符串,同時我們還可以將他作為數據源,然後使用>>操作符流出我們需要的類型,所以,他的妙用就是作為類型的轉換工具,而這一點使用spintf或者atoi等這些函數是很難做到的,關於這一點大家可以想想是為什麼。
//+--
template<class L,class R>
void convert(L& val,const R& right)
{
stringstream os;
if(!(os<<right))
return;
os>>val;
}
//+--
這個小工具可以將右邊的類型轉換到左邊的類型,我們可以這樣使用:
//+
int main(){
const char* ch = "100.568";
doubel val = 0;
convert(val,ch)
cout<<val<<endl;
return 0;
}
//+--
是不是很是方便,……嗯,但是他帶來一個問題,比如說:
//+--
int a = 10;
long b;
convert(b,a);
//+---
這種情況下我們原本是可以使用 b = a 來直接進行賦值的,但是由於使用了統一的操作接口,我們卻讓事情變得更加複雜啦,所以現在我們要解決一個問題,也就是說,如果我們可以直接使用b = a 時我們就使用 b = a,只有在不能使用 b = a 的時候才進行上面的轉換操作。問題回到了我們的上面的假設場景啦。
什麼時候能夠使用b = a 呢?從面向對象的角度來分析的話就是只有下面兩種情況能夠使用該操作:
//+--
class A;
class B{
//
//
//
B(const A&);
B& operator=(const A&);
};
//+-
那麼問題又來了,我們如何判斷B是否有這種成員函數呢?而對於基本類型來說又沒有這些成員函數——比如上面的int,double,long……等這些基礎類型,那麼我們又當如何處理呢?所以檢查是否存在賦值函數的存在的方法不可取,我們只有另闢蹊徑,我們可以將範圍放得更大一些,直接檢查是否存在可隱式轉換,而重載函數的試探正好可以解決這個問題,聽起來是不是有些玄妙,好吧,我們不妨來試試,畢竟直截了當的代碼勝過含糊其辭的千言萬語:
//+
template<class T,class U>
class MConvertsion{
static __int64 test(T);
static __int8 test(...);
static U genU();
enum{value = (sizeof(test(genU())) == sizeof(__int64))};
};
//+
就是這麼簡單,我們使用兩個重載函數test,一個有指定的類型作為參數,一個是變參,但是他們的返回類型不同,所以我們可以針對返回類型的不同進而判斷出U是否可以轉換到T,而這些函數都不需要實現,因為我們可以在編譯期就完成了這個判斷,如果你們現在還在懷疑這段程序的可執行性,那麼你們不妨親自測試一下:
//+----
std::cout << MConvertsion<int, std::string>::value << std::endl;
std::cout << MConvertsion<int, long>::value << std::endl;
//+----
我們經過測試,程序能夠很好的判斷是否能夠進行轉換,所以接下來我們可以完成我們上面的類型轉換工具了,我們將轉換的過程分兩步,一步是可以進行轉換操作的,一步是不可進行轉換操作的,我們可以使用bool變量來進行表示,但是下面的操作是不能夠工作的:
//+---
template<class L,class R>
void convert(L& val,const R& right)
{
if(MConvertsion<L, R>::value){
val = right;
}
else{
stringstream os;
if(!(os<<right))
return;
os>>val;
}
}
//+----
上面的程序在MConvertsion<L, R>::value == false 的時候將無法通過編譯,雖然if...else...就算不執行塊也必須要通過編譯,所以說到底if...else...是運行期的分發,而我們要解決的是編譯期的分發,所以我們只能使用模板來派發,當編譯變量為true的時候我們編譯第一步,為false的時候我們編譯第二步,實現代碼如下:
//+----
template<bool>
struct MCopyValue{
template<class T,class U>
static void apply(T& val1,const U& val2){
val1 = val2;
}
};
template<>
struct MCopyValue<false>{
template<class T,class U>
static void apply(T& val1,const U& val2){
std::stringstream ss;
ss<<val2;
ss>>val1;
}
template<class U>
static void apply(std::string& str,const U& val2){
std::stringstream ss;
ss<<val2;
str = ss.str();
}
};
//+-
我們將bool作為模板類型,該類型在編譯期間能夠直接被確認,如果為false的時候就直接編譯MCopyValue<false>,否則就編譯MCopyValue<true>,那麼該模板類型由誰來提供呢?當然就是MConvertsion<L, R>::value :
//+-
template<class L,class R>
void convert(L& val,const R& right)
{
MCopyValue<MConvertsion<L, R>::value>::apply(val,right);
}
int main(){
std::string str;
convert(str,123);
std::cout<<str<<std::endl;
long a = 10;
convert(a,str);
std::cout<<a<<std::endl;
int i = 0;
convert(i,a);
std::cout<<i<<std::endl;
double d = 0.0;
convert(d,i);
std::cout<<d<<std::endl;
return 0;
}
//+----
如果我們使用步進模式來調試上面的程序,我們會很明確的看到程序每一段都走進自己認為效率最好的代碼段中。
最後,關於C++的標準流的講解就到此為止,接下來我們將回過頭來看看C++中最為重要的關鍵字——class。
--
和流相關的章節:
printf()(1)
printf()(2)
上一章 : 深入淺出IO流
============================
回復 D 查看目錄,回複數字查看章節