第二章 stringstream的使用

2021-03-02 C/C++的編程教室

如果按照內容來給一個標題的話,那麼這一講的內容其實是上一講的後續部分,所以都屬於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 查看目錄,回複數字查看章節

相關焦點

  • C/C++編程筆記:C ++中的stringstream及其應用
    stringstream將字符串對象與流相關聯,使您可以像從流中讀取字符串一樣(例如cin)。
  • Java學習進階-Stream流
    = Stream.of(arr); Stream<String> stringStream1 = Stream.of("謝霆鋒", "王寶強", "賈乃亮", "陳羽凡"); }}小結- 使用Collection的stream()方法- 使用Stream流的of()方法package
  • Java Stream
    ("java8","lambda","stream");        Stream<String> stringStream = lists.stream();        Consumer<String> consumer = (x) -> System.out.println(x);        stringStream.forEach
  • Beetlex.Redis之Stream功能詳解
    接下來看一下插入對象的調用RedisStream<Employee> stream = DB.GetStream<Employee>("employees_stream");var id = await stream.Add(DataHelper.Defalut.Employees[0]);id = await stre
  • c++ fstream + string 處理大數據
    之前處理文本數據時,各種清洗數據用的都是java的File,FileReader/FileWriter,BufferedReader/BufferedWriter等類,詳見java讀寫文件(2)應用java的原因是java裡面的map非常靈活,eclipse編譯器更是給力,而且ctrl可以追蹤函數等,詳見java map的排序(3)應用java的另一個原因是java裡面的string
  • L2-數據結構-第05課 字符串string
    如果 s 是一個 string 對象且 s 不空,則 s[0] 就是字符串的第一個字符, s[1] 就表示第二個字符,而 s[s.size() - 1] 則表示 s 的最後一個字符。定義了三個類:istringstream、ostringstream 和 stringstream,分別用來進行流的輸入、輸出和輸入輸出操作。
  • Java 8 中處理集合的優雅姿勢——Stream
    以下代碼片段使用 filter 方法過濾掉空字符串:List<String> strings = Arrays.asList("Hollis", "", "HollisChuang", "H", "hollis");strings.stream().filter(string -> !
  • Java 8中處理集合的優雅姿勢——Stream
    以下代碼片段使用 filter 方法過濾掉空字符串:List<String> strings = Arrays.asList("Hollis", "", "HollisChuang", "H", "hollis");strings.stream().filter(string -> !
  • c++ fstream + string處理大數據
    (4)上面兩點算是自己的誤解吧,因為c++裡面也有也有與之對應的fstream類,c++map容器類,詳見c++map簡介(5)c++裡面也有相對比較成熟的string類,裡面的函數也大部分很靈活,沒有的也可以很容易的實現split,strim等,詳見c++string實現(6)最近從網上,看到了一句很經典的話,c++的風fstream類+string
  • Java 8的Stream代碼,你能看懂嗎?
    以下代碼片段使用 filter 方法過濾掉空字符串:List<String> strings = Arrays.asList("Hollis", "", "HollisChuang", "H", "hollis");strings.stream().filter(string -> !stri
  • c++中的string常用函數用法總結
    好了,進入正題………首先,為了在我們的程序中使用string類型,我們必須包含頭文件 <string>。c_str()返回一個以『/0'結尾的字符數組,而copy()則把字符串的內容複製或寫入既有的c_string或 字符數組內。C++字符串並不以'/0'結尾。我的建議是在程序中能使用C++字符串就使用,除非萬不得已不選用c_string。由於只是簡單介紹,詳細介紹掠過,誰想進一步了解使用中的注意事項可以給我留言(到我的收件箱)。我詳細解釋。
  • 標準 C++ 中的 string 類的用法總結
    的確,MFC中的CString類使用起來真的非常的方便好用。但是如果離開了MFC框架,還有沒有這樣使用起來非常方便的類呢?答案是肯定的。也許有人會說,即使不用MFC框架,也可以想辦法使用MFC中的API,具體的操作方法在本文最後給出操作方法。其實,可能很多人很可能會忽略掉標準C++中string類的使用。標準C++中提供的string類得功能也是非常強大的,一般都能滿足我們開發項目時使用。
  • 使用 C++ 的 StringBuilder 提升 4350% 的性能
    有些會建議(你)使用std::accumulate,這可以完成幾乎所有你要實現的:#include <iostream>// for std::cout, std::endl#include <string>  // for std::string#include <vector> // for std::vector
  • 【leetcode】1662.Check Two String are Equivalent&1108.an IPAddress
    檢查兩個字符串數組是否相等Given two string arrays word1 and word2, return true if the two arrays represent the same string, and false otherwise.
  • .NET使用MailKit進行郵件處理
    MailKit是最流行且最強大的.NET郵件處理框架之一,下面為大家簡單介紹MailKit的使用方式(IMAP為例)1.").Or(SearchQuery.SubjectContains("MailKit")); var uids = client.Inbox.Search(query);讀取方式二:讀取所有郵件 var uids = client.Inbox.Search(SearchQuery.All);操作郵件一:讀取郵件標題 string
  • 使用 Centos stream 8系統!
    相信這幾天大家都看過類似的文章了,看完這些文章後,很多人連Centos stream 8是什麼?都沒有搞清楚。就是記住一個:centos8停止更新了,以後不能使用centos8了,企業以後完了,我也不能考RHCE8了等等想法。我是MK老師,國內最早幾期通過RHCE,RHCA認證,擁有10多年的Linux實戰經驗和教學經驗。在此,我告訴大家,不用慌張!
  • C++之旅-string
    前言標準庫類型string表示可變長字符序列,使用之前需要包含string頭文件,它定義在命名空間std中。
  • Java Stream Pipeline 流水線:Stream 基礎
    所以留給我們的只有如下的兩種解決方式,通過使用 IntMapper 作為參數返回一個 IntStream 對象的方式對 map 方法進行重載,由此為 int stream 提供一種獨立的抽象。int sumOfLengths = strings.stream() .map(String::length) .sum();使用上述的方式就需要能夠使得 map(Function<T,U>) 能夠被
  • 你還以為使用 StringBuffer 就萬事大吉了?
    A string buffer is like a String, but can be modified.StringBuffer可以安全的在多線程場景下使用。事實真的是這樣的嗎?還真不是。雖然StringBuffer的大部分方法都加了synchronized修飾,但是在真實使用場景下只能說然並卵了,基本上任何出現StringBuffer的地方都可以用StringBuilder去替換。
  • foreach中遍歷list中string.contains的使用
    現在講有三個string類型list,列表中數據如下;三者的關係如圖所示,這裡需要得到圖中紅色部分;主要B、C中包含A的的字符串即可,不需要完全相同;這裡如果直接按照列表中進行遍歷可以直接通過foreach進行遍歷;代碼1:using System.Collections