第二章 stringstream的使用

如果按照内容来给一个标题的话,那么这一讲的内容其实是上一讲的后续部分,所以都属于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

发表评论