第一章 深入浅出IO流

如果没有iostream
如果没有iostream,那么我们的第一个程序应该怎么写呢?如果有iostream,我们的第一个程序成通常会是:
//+——————–
#include <iostream>
int main(){
std::cout<<“Hello World”<<std::endl;


return 0;


}
 

//+———————
 

这通常是我们学习C++最开始引入的程序,这个程序依赖了C++标准库的输出流ostream的对象cout,那么问题来了,如果我们自定义了一个类型,简单一点比如MyString,那么我们没有理由不让它支持打印这个操作,这时候如果没有iostream,我们应该怎么办呢?
//+———————–
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 <iostream>
 

int main(){
MyString str = “Hello World”;


std::cout<<str<<std::endl;


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<n;

++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<<str;


return 0;


}
 

//+————————–
 

注意了,我们这里使用的是stdout而不是std::cout,stdout是C语言定义的标准输出,也就是控制台,而std::cout是C++的ostream的对象,目的地也是控制台,他是对stdout的封装。回到我们上面的代码,这种写法没毛病,但是很别扭,尤其在C++里面,所以我们应该尝试对stdout进行封装,如下:
 

//+————————-
 

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<n;

++i){
putc(*ptr++,stdout)

}


}

};


 

 

 

OStream gOut;


int main(){
MyString str = “Hello World”;


gOut<<str;


return 0;


}
 

 

//+——————–
 

现在编译上面的程序我们会到编译错误,没有相应的操作符供我们使用,不过这都不是事,我们只需要扩展一下我们的operator<<操作符即可,我们可以使用模板让他做更多的事:
 

 

//+———————
 

template<class T>
T&

operator<<(T&

io,const MyString&

str){
sendstr(io,str.c_str(),str.size());


return io;


}
 

//+——————–
 

现在上面的代码能够正常工作了,当我们运行程序会在黑乎乎的控制台上看到Hello World的输出,那么如果我们想要使用std::ostream来输出呢?这时候我们应该怎么做呢?我们说了,我们使用模板来实现operator<<操作符,目的就是让他可以做更多事,所以对于std::ostream来说,MyString一样可以支持,只需要我们重载一个sendstr即可,如下:
 

 

//+——————–
 


void sendstr(std::ostream&

os,const char* ptr,int n){
os.write(ptr,n);


}
 

 

int main(){
MyString str = “Hello World”;


std::cout<<str;


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”<<std::endl;


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::endl;

为什么此处不能理解为将std::endl塞进std::cout中呢?原因很简单,因为std::endl是一个函数指针,他大致可以定义为下面的样子:
 

//+——————-
 

std::ostream&

endl(std::ostrem&

os){
os<<“n”;


os.flush();


return os;


}
 

//+——————
 

 

 

看到这里是不是觉得C++很有趣呢?那么问题来了,std::endl又是如何与流操作符联系上的呢?解决方式有很多,这里我们简单的说一种方式:
 

//+——————-
 

typedef std::function<std::ostream&

(std::ostream&

)>streamoperationtype;


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 = “<<A<<” = “<<int(A)<<endl;


}
 

//+——————–
 

write可以将一段buffer写到指定地方,所以换句话说,write除了写字符串之外还能够将数据按照二进制的放写进文件储存起来。
 

我们再来看另一个细节:
 

//+——————-
 

int main(){
cout<<true<<“t”<<
false<<endl;


}
 

//+——————-
 

运行程序,我们得到的结果是1和0,那么我们可不可以希望他输出的是true和false,当然可以:
 

//+——————-
 

#include <iostream>
#include <iomanip>
 

int main(){
cout<<true<<“t”<<
false<<endl;


cout<<boolalpha;


cout<<true<<“t”<<
false<<endl;


}
 

输出结果:
1 0;


true false;


 

//+——————-
 

boolalpha 在iomanip中定义,当使用他之后所有的bool类型操作将按照字符形式打印。
 

下面这个函数有点特殊:
 

//+——————-
 

ostream&

operator<<(const void*);


 

//+——————-
 

但他却让我们打印指针成了可能,很多时候我们确实很需要打印指针,当我们需要追踪一个对象的时候:
 

//+——————-
 

int main(){
int* ptr = new int(10);


cout<<&

p<<“t”<<p<<endl;


}
 

输出结果:
 

0x7788ff45 0x7895f2ff
 

//+——————-
 

对于内置的类型标准库都为我们实现了流的操作符,那么对于我们自定义的类型,如果我们有需要的话那就需要我们自行定义了,比如:
 

//+——————
 

class MInt{
public:
MInt(int val):mVal(val){ }
MInt(const MInt&

other):mVal(other.mVal){ }
private:

int mVal;


};


 

int main(){
MInt a(10);


cout<<a<<endl;


}
 

//+——————
 

上面的程序没法通过编译,因为cout无法对MInt操作。
cout同样没有对char实现流操作符,但是却提供了一个全局函数来操作char,让char如同其他内置类型一样可以使用流操作,这是一个思路,当然也是规则,重载流操作符的形式必须如下:
 

//+—————–
 

ostream&

operator<<(ostream&

os,const T&

other);


 

//+—————–
 

 

上面的 T 是就是要操作的类型,针对上面的MInt,可以如下:
 

 

//+——————
 

ostream&

operator<<(ostream&

os,const MInt&

other){
os<<other.mVal;


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<<a<<endl;


}
 

//+—————
 

现在一切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<class T>
istream&

operator>>(T&

val);


 

//+——————
 

但是很多时候我们不能这样写,为什么呢?回顾上面我们说过的ostream,或者简单点总结一下:如果我们想要用一个对象去操作另一个对象,那么该样式如下:
 

//+——————
 

template<class T,class A,class B>
T op(A a B b);


 

//+——————
 

这个函数的意义我们可以简单的理解为A使用op操作B返回T,按照这个思想我们来看看下面的声明表示的意义:
 

//+——————
 

template<class T>
T operator+(const T&

left,const T&

right);

// 表示 T res = left + right;


 

template<class T>
T operator-(const T&

left,const T&

right);

// 表示 T res = left – right;


 

 

template<class T>
T operator*(const T&

left,const T&

right);

// 表示 T res = left*right;


 

template<class T>
T operator/(const T&

left,const T&

right);

// 表示 T res = left/right;


 

….
 

 

//+——————–
 

 

现在我们回头来看看让cin使用>>来操作我们的对象,cin对应的是上面我们的left,而我们自己的对象就是上面对应的right,而返回的对象依然还是cin,所以要实现这个功能我们只需要实现诸如下面类型的函数:
 

 

//+——————-
 

template<class T>
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<<other.mVal;



}

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;


cout<<a<<endl;


MInt b = 10;


MInt c = a + b;


MInt d = c*a;


MInt e = d/c;


cout<<b<<endl<<c<<endl<<d<<endl<<e<<endl;


return 0;


}
 

//+——————-
 

 

这一章虽然在说iostream,但是内容却不局限于iostream,和其他的C++书籍比起,我们可能比较超前了些,不过我觉得以这样的方式开局应该是不错的,我属于这样的人,在接触到一门语言的时候首先考虑的是如何快如的建立交互,所以第一章的时候我们边开始思考流操作符重载这些问题,当然这些操作符重载是C++所必须的知识点,但又是一个难点,所以这些内容还是不太适用于从没接触过编程的初学者,因为我们并没有像教科书一般从基本类型开始,关于这一点,曾和同学讨论过,他认为这种难度的东西会不会吓退一部分人,我说不会,我们深入浅出的说,如果实在是对原理不甚清楚,但到后面知道怎么简单使用这也足够了,并且之所以会认为我们的第一章的难度颇大,是因为我们第一章几乎涵盖了C++的大部分知识要点,比如操作符重载,操纵器连杆器这些高级知识等,但这不些仅仅是为了让大家在C++的领域里面不只是浅尝辄止,不仅知其然而且更知其所以然,当然,这些东西后续会细说的,因为上一版很多东西就说的不清不楚,所以这一次会采取教训,当然大家也可以即使反馈。

发表评论