如果沒有iostream
如果沒有iostream, 那麼我們的第一個程式應該怎麼寫呢?如果有iostream, 我們的第一個程式成通常會是:
//+--------------------
#include
int main(){
std::cout<<"Hello World"< return 0; } //+--------------------- 這通常是我們學習C++最開始引入的程式,
這個程式依賴了C++標準庫的輸出流ostream的物件cout,
那麼問題來了,
如果我們自訂了一個類型,
簡單一點比如MyString,
那麼我們沒有理由不讓它支援列印這個操作,
這時候如果沒有iostream,
我們應該怎麼辦呢?順便推薦下我自己的C/C++學習群:598131849,
不管你是小白還是大牛,
小編我都挺歡迎,
不定期分享乾貨,
包括我自己整理的資料和零基礎入門教程,
送給大家,
歡迎初學和進階中的小夥伴。
//+-----------------------
class MyString{
//
// 實現細節
//
};
//+-----------------------
通常來說, 如果我們需要讓我們的類型支援流的的輸出, 那麼我們就會考慮如下的方式:
//+-----------------------
std::ostream& operator<<(std::ostream& os,const MyString& str)
//
// 實現細節
//
return os;
}
//+-------------------------
這種方式是我們常用的方式, 是每一個C++程式師必須第一時間掌握的實現方式, 下面我們會細說。 現在的問題是這個函數我們應該怎麼實現才能減少設計上帶來的耦合, 我們可以這麼做:
//+-------------------------
namespace std{
class ostream;
}
std::ostream& operator<<(std::ostream& os,const MyString& str);
//+-------------------------
注意, 我們這裡僅聲明不實現, 我們在其他的地方單獨定義該函數, 這樣可以減少我們的類型MyString對iostream的耦合, 但是在我們定義該函數的地方必須要包含iostream標頭檔, 否則ostream就是未定義的類, 當我們實現了這個函數之後, 我們便可這麼使用:
//+------------------------
#include "MyString.h"
#include
int main(){
MyString str = "Hello World";
std::cout< return 0; } //+------------------------ 這個問題解決了設計上的耦合問題,
但是並沒有解決上面我們所提出的問題:如果我們沒有iostream的話呢? 沒有iostream沒有關係,
iostream依賴於C++標準庫,
但是有不依賴於C++ 標準庫的東西,
比如put系列函數,
該系列函數是C運行庫提供的函數,
由於C語言是很多程式設計語言的祖先,
而C++更是C的擴展,
所以在C++中使用C函數是毫無問題的,
但是此處使用put系列函數顯得太不C++,
當然可以對其進行封裝再使用——物件導向程式設計不就是這樣的嗎?到這一步,
我們可以來考慮使用類的概念來對put系列函數的封裝,
此處我們選擇使用putc,
putc函數的作用是將一個字元寫進指定的檔中,
這剛好滿足我們的需求。
將資料寫進目標位址中,
秉著這一概念,
我們可以先為我們的io定義一個函數: //+------------------------ void sendstr(FILE* fp,const char* str,int n) for(int i=0;i putc(*str++,fp); } } //+------------------------- 現在回到我們的MyString上面: //+------------------------- class MyString{...}; FILE* operator<<(FILE* fp,const MyString& str) sendstr(fp,str.c_str(),str.size()); return fp; } //+------------------------- 有了這個操作符之後我們便可以寫出下面的代碼: //+------------------------- int main(){ MyString str = "Hello World"; stdout< return 0; } //+-------------------------- 注意了,
//+------------------------- class MyStrIobase{ public: virtual ~MyIobase(){} virtual void send(const char* ptr,int n){} }; //+------------------------ 這是一個介面操作,
有了這個介面,
再加上我們上面的概念,
所以我們可以對sendstr進行擴展: //+------------------------ void sendstr(MyStrIobase& strio,const char* ptr,int n) strio.send(ptr,n) } //+------------------------ 現在還沒法工作,
因為我們的MyStrIoBase啥都沒做,
如果我們想要做些什麼,
那麼就需要從MyStrIoBase派生出我們自己的類,
比如列印到控制台的OStream: //+----------------------- class OStream : public MyStrIobase{ public: virtual void send(const char* ptr,int n){ for(int i=0;i putc(*ptr++,stdout) } } }; OStream gOut; int main(){ MyString str = "Hello World"; gOut< return 0; } //+-------------------- 現在編譯上面的程式我們會到編譯錯誤,
沒有相應的操作符供我們使用,
不過這都不是事,
我們只需要擴展一下我們的operator<<操作符即可,
我們可以使用範本讓他做更多的事: //+--------------------- template T& operator<<(T& io,const MyString& str){ sendstr(io,str.c_str(),str.size()); return io; } //+-------------------- 現在上面的代碼能夠正常工作了,
//+-------------------- void sendstr(std::ostream& os,const char* ptr,int n){ os.write(ptr,n); } int main(){ MyString str = "Hello World"; std::cout< return 0; } //+-------------------- 程式如同我們預期正常工作,
那麼,
現在我們再回過頭去看看我們的MyStrIobase,如果我們想要讓他對檔的支持,
那麼我們應該怎麼做呢?這些問題如果放到後面來說可能很簡單,
當然放在這裡說也是很簡單的,
我們對FILE*進行簡單的封裝即可: //+-------------------- class FileOStream : public MyStrIobase{ public: FileOStream(const char* fileName,const char* mode){ mFile = fopen(fileName, mode); } virtual void send(const char* ptr, int n){ if (mFile == nullptr) return; for (int i = 0; i < n; ++i){ putc(*ptr++, mFile); } } void close(){ fclose(mFile); mFile = nullptr; } private: FILE* mFile{ nullptr }; }; int main(){ MyString str = "Hello World"; FileOStream out("text.txt","w+"); out << str; out.close(); return 0; } //+--------------------- 運行程式後我們便將字元寫進了檔之中。
----------------------------------- iostream 到此,
我們對io的操作有了一個大概的瞭解,
接下裡我們深入的觀察一下iostream的工作原理,
//+---------------------- int main(){ std::cout<<"Hello World"< return 0; } //+--------------------- 當我們執行上面程式的時候會發現控制台上列印出Hello World字元,
同時游標移動到下一行的起始處。
我們用一句通俗的話來對這句代碼的描述:將"Hello World"塞進std::cout中,
然後std::endl操作std::cout,
有了前面的基礎,
我們要理解這句話不難,
程式是由左向右執行,
所以第一步的執行是如下的函數: //--------------------- std::ostream& operator<<(std::ostream& os,const char* msg){ os.write(msg,strlen(msg)); return os; } //+-------------------- 在執行完上面這句代碼之後返回std::cout,所以接下來執行的確實:std::cout< //+------------------- std::ostream& endl(std::ostrem& os){ os<<" "; os.flush(); return os; } //+------------------ 看到這裡是不是覺得C++很有趣呢?那麼問題來了,
std::endl又是如何與流操作符聯繫上的呢?解決方式有很多,
這裡我們簡單的說一種方式: //+------------------- typedef std::function std::ostream& operator<<(std::ostream& os,streamoperationtype fun){ return fun(os); } //+---------------- 是不是很有意思……當然標準庫可不是這麼幹的,
這只是我個人這麼幹,但是思路應該是一致的,就算不一致,至少我們又知道了另一種解決方案。那麼,列印在控制台的操作是不是只有cout呢?當然不是,下面是vs中的iostream聲明的幾個流物件,w開頭的是針對unicode字元的流物件,常用的是cin和cout以及cerr,至於clog是用於輸出日誌的,它寫的目的地和cerr一樣都是stderr。 //+---------------- __PURE_APPDOMAIN_GLOBAL extern _CRTDATA2 istream cin, *_Ptr_cin; __PURE_APPDOMAIN_GLOBAL extern _CRTDATA2 ostream cout, *_Ptr_cout; __PURE_APPDOMAIN_GLOBAL extern _CRTDATA2 ostream cerr, *_Ptr_cerr; __PURE_APPDOMAIN_GLOBAL extern _CRTDATA2 ostream clog, *_Ptr_clog; __PURE_APPDOMAIN_GLOBAL extern _CRTDATA2 wistream wcin, *_Ptr_wcin; __PURE_APPDOMAIN_GLOBAL extern _CRTDATA2 wostream wcout, *_Ptr_wcout; __PURE_APPDOMAIN_GLOBAL extern _CRTDATA2 wostream wcerr, *_Ptr_wcerr; __PURE_APPDOMAIN_GLOBAL extern _CRTDATA2 wostream wclog, *_Ptr_wclog; //+----------------- 在ostream裡面定義了處理內部資訊的輸出操作符: //+---------------- ... ostream& operator<<(short); ostream& operator<<(int); ostream& operator<<(long); ostream& operator<<(long long); ostream& operator<<(unsigned short); ostream& operator<<(unsigned int); ostream& operator<<(unsigned long); ostream& operator<<(unsigned long long); ostream& operator<<(float); ostream& operator<<(double); ostream& operator<<(long double); ostream& operator<<(bool); ostream& operator<<(const void*); ostream& put(char); ostream& write(const char*,streamsize); ... //+------------------ 我們看到裡面缺少了對char的操作符,但是put和write可以簡單地寫出字元,所以就沒必要實現一個流操作符(這是C++之父所說,因為標準就是這麼定義),可以通過全域函數來實現: //+------------------ ostream& operator<<(ostream& os,char ch){ os.put(ch); return os; } ostream& operator<<(ostream& os,const char* msg){ os.write(msg,strlen(msg)); return os; } int main(){ char A('A'); cout<<"A = "< } //+-------------------- write可以將一段buffer寫到指定地方,所以換句話說,write除了寫字串之外還能夠將資料按照二進位的放寫進檔儲存起來。 我們再來看另一個細節: //+------------------- int main(){ cout< } //+------------------- 運行程式,我們得到的結果是1和0,那麼我們可不可以希望他輸出的是true和false,當然可以: //+------------------- #include #include int main(){ cout< cout< cout< } 輸出結果: 1 0; truefalse; //+------------------- boolalpha 在iomanip中定義,當使用他之後所有的bool類型操作將按照字元形式列印。 下面這個函數有點特殊: //+------------------- ostream& operator<<(const void*); //+------------------- 但他卻讓我們列印指標成了可能,很多時候我們確實很需要列印指標,當我們需要追蹤一個物件的時候: //+------------------- int main(){ int* ptr = new int(10); cout<<&p<<" "< } 輸出結果: 0x7788ff450x7895f2ff //+------------------- 對於內置的類型標準庫都為我們實現了流的操作符,那麼對於我們自訂的類型,如果我們有需要的話那就需要我們自行定義了,比如: //+------------------ class MInt{ public: MInt(int val):mVal(val){} MInt(const MInt& other):mVal(other.mVal){} private
: int mVal; }; int main(){ MInt a(10); cout< } //+------------------ 上面的程式沒法通過編譯,因為cout無法對MInt操作。 cout同樣沒有對char實現流操作符,但是卻提供了一個全域函數來操作char,讓char如同其他內置類型一樣可以使用流操作,這是一個思路,當然也是規則,重載流操作符的形式必須如下: //+----------------- ostream& operator<<(ostream& os,const T& other); //+----------------- 上面的 T 是就是要操作的類型,針對上面的MInt,可以如下: //+------------------ ostream& operator<<(ostream& os,const MInt& other){ os< return os; } //+---------------- 現在又一個問題來了,mVal是MInt的私有變數,所以是不能直接訪問的,但是除此之外又沒他發,當然可以給MInt提供一個介面讓他返回mVal,不過除此之外還是有其他辦法的——友元函數。 //+---------------- class MInt{ public: MInt(int val):mVal(val){} MInt(const MInt& other):mVal(other.mVal){} friend ostream& operator<<(ostream& os,const MInt& other); private: int mVal; }; int main(){ MInt a(10); cout< } //+--------------- 現在一切ok,可以看到想像中的結果。 和ostream一樣,istream同樣針對內置類型都實現輸入流操作符。 //+--------------- istream& operator>>(short&); istream& operator>>(int&); istream& operator>>(long&); istream& operator>>(long long&); istream& operator>>(unsigned short&); istream& operator>>(unsigned int&); istream& operator>>(unsigned long&); istream& operator>>(unsigned long long&); istream& operator>>(float&); istream& operator>>(double&); istream& operator>>(long double&); istream& operator>>(bool&); istream& operator>>(void*&); istream& get(char); ... //+------------------ 想要通過cin來初始化物件,我們只需要實現相應的函數即可,而這個函數的樣子如下: //+------------------ template istream& operator>>(T& val); //+------------------ 但是很多時候我們不能這樣寫,為什麼呢?回顧上面我們說過的ostream,或者簡單點總結一下:如果我們想要用一個物件去操作另一個物件,那麼該樣式如下: //+------------------ template T op(A a B b); //+------------------ 這個函數的意義我們可以簡單的理解為A使用op操作B返回T,按照這個思想我們來看看下面的聲明表示的意義: //+------------------ template T operator+(const T& left,const T& right); // 表示 T res = left + right; template T operator-(const T& left,const T& right); // 表示 T res = left - right; template T operator*(const T& left,const T& right); // 表示 T res = left*right; template T operator/(const T& left,const T& right); // 表示 T res = left/right; .... //+-------------------- 現在我們回頭來看看讓cin使用>>來操作我們的物件,cin對應的是上面我們的left,而我們自己的物件就是上面對應的right,而返回的物件依然還是cin,所以要實現這個功能我們只需要實現諸如下面類型的函數: //+------------------- template istream& operator>>(istream& is,T& res); //+------------------- 此處 T 表示我們想要表達的類型,當然都是一些複合類型,但是複合類型是由簡單類型組合而成,所以我們在具體實現的時候只要針對複合類型的資料成員進行cin即可,比如: //+------------------- class MInt{ public: MInt(int val = 0):mVal(val){} MInt(const MInt& other):mVal(other.mVal){} friend ostream& operator<<(ostream& os,const MInt& other){ os< } friend istream& operator>>(istream& is,MInt& out){ is>>out.mVal; return is; } friend MInt operator+(const MInt& left,const MInt& right){ return MInt(left.mVal + right.mVal); } friend MInt operator-(const MInt& left,const MInt& right){ return MInt(left.mVal - right.mVal); } friend MInt operator*(const MInt& left,const MInt& right){ return MInt(left.mVal*right.mVal); } friend MInt operator/(const MInt& left,const MInt& right){ return MInt(left.mVal/right.mVal); } private: int mVal; }; int main(){ MInt a; cin>>a; //+---------------- __PURE_APPDOMAIN_GLOBAL extern _CRTDATA2 istream cin, *_Ptr_cin; __PURE_APPDOMAIN_GLOBAL extern _CRTDATA2 ostream cout, *_Ptr_cout; __PURE_APPDOMAIN_GLOBAL extern _CRTDATA2 ostream cerr, *_Ptr_cerr; __PURE_APPDOMAIN_GLOBAL extern _CRTDATA2 ostream clog, *_Ptr_clog; __PURE_APPDOMAIN_GLOBAL extern _CRTDATA2 wistream wcin, *_Ptr_wcin; __PURE_APPDOMAIN_GLOBAL extern _CRTDATA2 wostream wcout, *_Ptr_wcout; __PURE_APPDOMAIN_GLOBAL extern _CRTDATA2 wostream wcerr, *_Ptr_wcerr; __PURE_APPDOMAIN_GLOBAL extern _CRTDATA2 wostream wclog, *_Ptr_wclog; //+----------------- 在ostream裡面定義了處理內部資訊的輸出操作符: //+---------------- ... ostream& operator<<(short); ostream& operator<<(int); ostream& operator<<(long); ostream& operator<<(long long); ostream& operator<<(unsigned short); ostream& operator<<(unsigned int); ostream& operator<<(unsigned long); ostream& operator<<(unsigned long long); ostream& operator<<(float); ostream& operator<<(double); ostream& operator<<(long double); ostream& operator<<(bool); ostream& operator<<(const void*); ostream& put(char); ostream& write(const char*,streamsize); ... //+------------------ 我們看到裡面缺少了對char的操作符,但是put和write可以簡單地寫出字元,所以就沒必要實現一個流操作符(這是C++之父所說,因為標準就是這麼定義),可以通過全域函數來實現: //+------------------ ostream& operator<<(ostream& os,char ch){ os.put(ch); return os; } ostream& operator<<(ostream& os,const char* msg){ os.write(msg,strlen(msg)); return os; } int main(){ char A('A'); cout<<"A = "< } //+-------------------- write可以將一段buffer寫到指定地方,所以換句話說,write除了寫字串之外還能夠將資料按照二進位的放寫進檔儲存起來。 我們再來看另一個細節: //+------------------- int main(){ cout< } //+------------------- 運行程式,我們得到的結果是1和0,那麼我們可不可以希望他輸出的是true和false,當然可以: //+------------------- #include #include int main(){ cout< cout< cout< } 輸出結果: 1 0; truefalse; //+------------------- boolalpha 在iomanip中定義,當使用他之後所有的bool類型操作將按照字元形式列印。 下面這個函數有點特殊: //+------------------- ostream& operator<<(const void*); //+------------------- 但他卻讓我們列印指標成了可能,很多時候我們確實很需要列印指標,當我們需要追蹤一個物件的時候: //+------------------- int main(){ int* ptr = new int(10); cout<<&p<<" "< } 輸出結果: 0x7788ff450x7895f2ff //+------------------- 對於內置的類型標準庫都為我們實現了流的操作符,那麼對於我們自訂的類型,如果我們有需要的話那就需要我們自行定義了,比如: //+------------------ class MInt{ public: MInt(int val):mVal(val){} MInt(const MInt& other):mVal(other.mVal){} private
: int mVal; }; int main(){ MInt a(10); cout< } //+------------------ 上面的程式沒法通過編譯,因為cout無法對MInt操作。 cout同樣沒有對char實現流操作符,但是卻提供了一個全域函數來操作char,讓char如同其他內置類型一樣可以使用流操作,這是一個思路,當然也是規則,重載流操作符的形式必須如下: //+----------------- ostream& operator<<(ostream& os,const T& other); //+----------------- 上面的 T 是就是要操作的類型,針對上面的MInt,可以如下: //+------------------ ostream& operator<<(ostream& os,const MInt& other){ os< return os; } //+---------------- 現在又一個問題來了,mVal是MInt的私有變數,所以是不能直接訪問的,但是除此之外又沒他發,當然可以給MInt提供一個介面讓他返回mVal,不過除此之外還是有其他辦法的——友元函數。 //+---------------- class MInt{ public: MInt(int val):mVal(val){} MInt(const MInt& other):mVal(other.mVal){} friend ostream& operator<<(ostream& os,const MInt& other); private: int mVal; }; int main(){ MInt a(10);