0%

IO类

在之前的一些用到输入与输出的程序都是用户对流的读写,也就是使用的iostream类,但是C++中的IO处理并不止通过流进行读写,为了支持不同的IO操作,标准库中还定义了其他的一些IO类,他们被包含在不同的头文件中:

头文件 作用
iostream (w)istream
(w)ostream
(w)iostream
普通流中读写数据
fstream (w)ifstream
(w)ofstream
(w)fstream
文件流中读写数据
sstream (w)istringstream
(w)ostringstream
(w)stringstream
字符串流中读写数据
  • 在类和其函数前添加w则为正常类对象、函数等(例如wcin,wcout)的宽字符版。

IO类型间的关系

类型ifstream和istringstream都继承自istream,因此,我们如何使用istream,就如何使用其他IO类型。在之后介绍的标准库的流特性均可以无差别地使用于各个IO类型

IO类型的特性

IO对象不能拷贝

当我们定义了一些IO对象时(通常是用于IO操作的一些函数),之前也使用过,是不能对这些对象进行拷贝或赋值的操作的,因此当我们操作流时,通常都是使用引用的方式传递和返回流,这也意味着这些IO操作函数的返回类型要定义为IO类的引用,参数中的流也需定义为IO类的引用。

1
2
3
4
5
istream &read(istream &in, person &item)
{
in>>item.name>>item.addr;
return in;
}//上一章讲到的read函数就是一个IO操作函数

条件状态

当使用IO操作流时可能会发生多种错误,为了查询这些错误,标准库中定义了相关函数和标准以访问当前流的条件状态。

流分为以下几个状态:(这些状态为IO类中定义)

badbit 当前流已崩溃
failbit 当前IO操作失败了
eofbit 当前流到达了文件结束
goodbit 为0代表流正常
  • 可以对这些状态位进行位运算
  • badbit表示系统级错误,是不可恢复的错误。通常一旦被置位,流就无法再使用了。
  • failbit表示可恢复的错误,修正后流可以机械使用。badbit被置位后failbit也会被置位
  • 如果文件结束,failbit和eofbit都会被置位
  • 上面三位任意一位有置位情况goodbit都会被置位

以下是IO类的一些查询函数:

类的子函数 作用 返回值
eof() 查询eofbit状态 若对应状态位置位则返回ture
fail() 查询failbit状态 若对应状态位置位则返回ture
bad() 查询badbit状态 若对应状态位置位则返回ture
good() 查询goodbit状态 若对应状态位置位则返回ture
clear() 将流中所有条件状态位复位 void
rdstate() 返回当前流的状态 一种包含所有状态位信息的类,但类型同状态位
clear(flag) 复位指定状态位 void
setstate(flag) 置位指定状态位 void

管理条件状态

每进行一次流操作后恢复上一次的条件状态:

1
2
3
4
auto oldstate = cin.rdstate();
cin.clear();
cin>>....//一次输入流操作
cin.setstate(oldstate);

通过位操作一次操作多个状态位:

1
2
//假设当前所有状态置位
cin.clear(cin.rdstate() & ~cin.failbit & ~cin.badbit)//只复位failbit,badbit

输出流的缓冲

刷新缓冲区

之前讲到过输出endl可以刷新缓冲区,还有以下操作也可以刷新

1
2
3
cout<<endl; //换行
cout<<flush; //不添加任何额外字符
cout<<ends; //添加一个空字符

刷新缓冲区的作用

我其实也不是特别理解,姑且谈一下我的理解,这是网上的一个例程

1
2
3
4
5
6
7
8
9
int main()
{
setvbuf( stdout, NULL, _IOLBF, 1024 ); //设置控制台输出为行缓存模式,把缓冲区与流相关
cout <<"hello world\n"<<endl;
Sleep(5000);
cout <<"leeboy" <<endl;
system("pause");
return 0;
}

如果第四行不刷新缓冲区,则4行内容会和7行在5s后一起输出,所以刷新缓冲区的作用应该是立即输出当前内容?而按照网上说法,如果不刷新缓冲区,输出是由系统每隔一段时间检测输出流是否有内容。

unitbuf操作符

如果想设置每一次输出后自动刷新缓冲区,可以:

1
2
cout<<std::unitbuf;
cout<<std::nounitbuf;//恢复正常缓冲模式

关联输入和输出流

关联后任何试图从输入流读取数据的操作都会先刷新关联的输出流

1
cin>>std::ival;//默认情况下cin和cerr都关联到了cout

文件的输入与输出

头文件fstream中定义了三个类来支持关于文件的IO操作:

  • ifstream:用于文件读操作,从给定文件读取内容
  • ofstream:用于文件写操作
  • fstream:读写操作,可以通过不同配置实现上两类的功能

这些类都继承了iostream的相关操作,同时还定义了一些新的成员来管理与流相关的文件,以下列出了部分特有操作,这些操作都适用于上三个类,但不能对其他IO类调用

操作 功能
fstream a; 创建一个未绑定的文件流
fstream a(s); 创建一个文件流,并打开名为s的文件,s为string类型或C风格字符串指针
a.open() 打开一个文件,并绑定至流a
a.close() 关闭绑定的文件
a.is_open() 返回一个bool值,指出文件是否成功打开

文件的打开

刚刚讲到了有两种方式打开一个文件,分别是

1
2
3
4
fstream a("dhk.txt");
///////////////////////
fstream b;
b.open("dhk.txt");

我们先详解一下open函数:

open()函数

根据输入的参数数量,该函数有以下几个定义

1
2
3
4
5
6
7
8
9
10
 
public member function

void open ( const char * filename,
ios_base::openmode mode = ios_base::in | ios_base::out );

void open(const wchar_t *_Filename,
ios_base::openmode mode= ios_base::in | ios_base::out,
int prot = ios_base::_Openprot)

参数:

  • filename:文件名
  • open mode:打开方式
  • prot:打开文件的属性,一般很少用到

这里重点讲一下打开方式,刚刚我们讲过:头文件fstream中的fstream类可以实现读和写的操作,也就是可以通过不同的配置实现ifstream和ofstream的操作,也就是通过这里的打开方式配置。

打开方式在ios类中定义:

ios::in 为输入(读)而打开文件
ios::out 为输出(写)而打开文件
ios::ate 初始位置:文件尾
ios::app 所有输出附加在文件末尾
ios::trunc 如果文件已存在则先删除该文件
ios::binary 二进制方式

这些方式可以通过或运算|组合使用

  • ate和binary通常用于打开任何类型的文件流对象

默认打开方式

对于第二种方式fstream a("dhk.txt");其实是隐式地调用了open()函数,直接使用了默认打开方式:

1
2
3
ofstream out("...", ios::out);
ifstream in("...", ios::in);
fstream foi("...", ios::in|ios::out);

以写模式打开一个文件会丢弃已有数据

默认情况下,当打开一个ofstream对象时,若没有指定模式,默认是隐含的使用截断(ios::trunc),即每次打开该文件时会清空之前的内容,若要阻止清空,需显式地设置为ios::app模式,同时,使用app模式则隐式地包含了ios::out。

使用fstream对象代替iostream&

由于fstream类继承自iostream,因此,接受一个iostream类型引用参数的函数也可以接收fstream类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 struct person
{
string name;
string addr;
};

istream &read(istream &in, person &item)
{
in>>item.name>>item.addr;
return in;
}

ostream &print(ostream &out, person &item)
{
out<<"NAME: "<<item.name<<endl;
out<<"ADDR: "<<item.addr<<endl;
return out;
}

int main()
{
person man;
ifstream in("D.txt");
ofstream out("dhk.txt");
read(in,man); //使用ifstream代替istream
print(out,man); //使用ofstream代替ostream
out.close();
return 0;
}

注意事项

对一个已经打开的文件流再调用open函数会导致该流的failbit置位,后续操作都会失败

  • 查看文件是否成功打开的快捷方式:if(a);

文件打开路径的问题

文件的默认打开路径我也没搞懂,最好使用相对路径打开相关文件

1
ifstream in(".vscode\\fstream\\D.txt");

并且这里要使用双斜杠!!!

字符串流

在头文件sstream中定义了三个类来支持操作字符串IO,分别是:

  • istringstream
  • ostirngstream
  • stringstream

这些类同样继承自iostream,用法同文件流,类特有操作有:

stringstream a; 创建一个未绑定string的流
stringstream a(s); 与string对象s绑定的一个字符串流
a.str(); 返回a中所保存的所有字符串
a.str(); 将s拷贝至a中,返回void
a.clear(); 清空流

读取string

与文件流相同,stringstream也可以实现读取和输入的功能

下面以一个例程说明string流的用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include<iostream>
#include<fstream>
#include<string>
#include<windows.h>
#include<sstream>
using namespace std;
struct person
{
string name;
string addr;
};
istream &read(istream &in, person &item)
{
in>>item.name>>item.addr;
return in;
}
ostream &print(ostream &out, person &item)
{
out<<"NAME: "<<item.name<<endl;
out<<"ADDR: "<<item.addr<<endl;
return out;
}

int main()
{
person man;
string line;
ifstream in("sstream\\D.txt"); //打开一个txt文件并绑定一个输入文件流in
/*D.txt的内容:
dhk 12
aca 34
*/
while(getline(in,line)) //分别处理文件中每一行的数据
{
stringstream s(line);//将该行内容与字符串流s绑定,s的寿命为当次循环,当次循环结束s自动销毁
s>>man.name;//将该行第一个词输入进类成员
s>>man.addr;//该行第二个词输入进类成员
print(cout,man);//通过iostream输出类成员
}
in.close();//关闭打开的文件
system("pause");
return 0;
}
  • 可以看出,流每次输出(入)的内容为一个“词”,也就是说空格和换行都会结束该次输出(入)的内容

输出到字符串流

当我们想逐步构造想要输出的内容,最后一口气打印时,就可以使用字符串的输出流,将构造好的内容分布写入到ostingstream(或stringstream的输出流)中,最后通过函数.str()输出到cout或者你想要的其他流中

位域结构体

有时候,信息存储的时候,并不需要一个完整的字节,而只需占几个或一个二进制位就能满足需求。例如存放一个bool类型的变量,只需要true或者false即可,这种情况下,只需要0或者1一位二进制位即可。为了节省存储空间,并使处理简便,C语言又提供了一种数据结构,称为“位域”或“位段”。所谓“位域”是把一个字节中的二进位划分为几个不同的区域,并说明每个区域的位数。每个域有一个域名,允许在程序中按域名进行操作。这样就可以把几个不同的对象用一个字节的二进制位域来表示。

格式:

1
2
3
4
5
6
7
struct _net_pro_cdu  
{
unsigned char acs:4
unsigned char :0
unsigned char ace:4
unsigned char dve:4
} Ncdu;

注意:

  • 无位域名的位域不能使用,仅用来填充、调整尾置(如第四行)
  • 位大小不能超过定义用的类型(如上述例子位数不能超过8)

volatile关键字

声明一个变量是易变的,可以防止编译器对程序一些变量运算的过度优化。

for循环执行顺序

1
2
3
for(int i =0; i<5;i++){
// 循环体
}

执行顺序解抛
执行的顺序如下:
第一步 : i=0 初始化值
第二步 : i<5 进行条件判断,如果为真,则继续执行
第三步 : 执行循环体的内容
第四步 : i++ 变量i自增
第五步 : 回到第二步,条件判断为真,则执行循环体内容,再到i++一直循环,
直到第二步的判断条件为假,则退出该循环

原文链接:https://blog.csdn.net/weixin_44080187/article/details/123429045

union类型

长度问题

实例化为变量后,这个变量的长度是这个union中最长的数据类型的长度

1
2
3
4
5
union DEMO{ 
char status;
int a;
int serial[4];
}demo;//大小为16字节

数据访问

对于union成员,地址相同,存储有点像并列一样的存储(?我是这么理解的),一个成员变化一定会影响其他成员

定义抽象数据类型

定义一个类

C++中可以使用struct和class构建一个类,struct为继承C语言留下,但用法与C不同。struct和class使用方法完全相同,只是对类成员默认的访问权限不同,这点后面会谈到。

1
2
3
4
5
6
struct/class person
{
string name;
string addr;
int test();
}

在类的外部定义成员函数

C++的struct与C最不同的一点就是C++可以在类中定义函数,例如上述类中声明了一个返回值为int型的函数。

该函数的内容可以在类中定义,也可以只在类中声明,再在外部定义。在外部定义时需注意以下几点:

  • 函数类型声明、函数名、参数列表必须和声明中一致
  • 由于函数是在类中声明,在定义时需声明其命名空间(声明函数名)
1
2
int person::test()
{}

定义一个返回类的函数

1
2
3
4
5
6
7
8
9
10
struct person
{
string name;
string addr;
person& copy(const person &x)
{
name=x.name;
return *this;
}
}
  • 该函数同样可以在类外定义,规则同上一节

  • 函数返回的类型必须为引用,因为返回的值应该为一个左值

  • 若函数参数也为一个类对象,也必须为引用格式

  • return *this意为返回该调用该函数的对象,例如:

    1
    2
    3
    4
    person test1,test2;
    test1.name="dhk"; test2.name="aca";
    test1.copy(test2);
    cout<<test1.name; //输出"aca"
  • 函数体中的name和addr为调用该函数的对象的类成员

定义类相关的非成员函数

通过定义流的引用直接操作类成员的输入输出

1
2
3
4
5
6
7
8
9
10
11
istream &read(istream &in, person &item)
{
in>>item.name>>item.addr;
return in;
}
ostream &print(ostream &out, person &item)
{
out<<"NAME: "<<item.name<<endl;
out<<"ADDR: "<<item.addr<<endl;
return out;
}
  • istream和ostream需提前声明其命名空间
  • IO类不能被拷贝,因此当其作为参数时必须通过引用传递值
  • 此处print函数的参数item可以不使用引用

类的构造函数

当我们定义一个类对象时,并没有对每一个类成员进行初始化,这时,编译器便通过一个特殊的构造函数来控制当前初始化,此函数叫做默认构造函数,该函数会对所有成员进行默认初始化。

但是如果我们想在定义类对象时就对一些成员进行我们想要的初始化该怎么办呢?这时我们就可以自己定义构造函数对一些类成员进行想要的初始化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct person
{
person()=default;
person(string s1, string s2) {name=s1;addr=s2;}
person(person &item) {copy(item);}
person(istream &in) {read(in,*this);}
/**********************以上为构造函数*******************/
string name;
string addr;
person& copy(const person &x)
{
name=x.name;
return *this;
}
}
istream &read(istream &in, person &item)
{
in>>item.name>>item.addr;
return in;
}

int main()
{
person a;
string s1("dhk"),s2("aca"); person b(s1,s2);
person c(b);
person d(cin);
}

在person类中一共定义了四个构造函数,下面将分别讲解这四个构造函数的含义和相关知识:

  • person()=default;默认构造函数

    当一个类中定义了其他构造函数时,编译器便不会自动生成默认构造函数。因此,若没有此函数,主函数中24行便会报错,原因是没有与当前定义情况匹配的构造函数。

  • person(string s1, string s2)

    如主函数25行,当初始化参数为两个string对象时,会进入该构造函数,该构造函数的作用是将类成员赋值为两个string对象。需要注意:

    • 大括号内为构造函数的函数体,可以在此完成一系列的初始化

    • 这里也有一种构造初始值列表的初始化方式:

      1
      person(string s1, string s2): name(s1),addr(s2) {}

      两种方式作用完全相同,但是实现的原理有所差异,这点后面会讲到。

  • person(person &item)

    如主函数26行,类初始化的参数也可以为一个类对象,该构造函数会直接将输入参数用于执行copy函数,相当于通过copy函数间接给类成员赋值。

  • person(istream &in)

    如27行,输入参数为一个IO类型的引用,需要注意此时read函数第二个参数为*this

构造函数初始值列表和赋值的区别

之前我们讲到了构造函数对类成员的操作有两种:

1
2
person(string s1, string s2) {name=s1;addr=s2;}
person(string s1, string s2): name(s1),addr(s2) {}

两个操作在效果上一样,但是第一种操作根源上为赋值,而第二种操作才是初始化,在大部分情况下二者区别不大但是如果当类成员为引用或者const类型时,此时将不能赋值,但是可以初始化:

1
2
3
4
5
6
7
struct person
{
person(int i) {a=i;r=i} //错误的,不能给引用和常量赋值
person(int i): a(i),r(i) {} //正确
const int a;
int &r;
}

访问控制和封装

前面讲到了使用class和struct唯一区别是对类成员默认的访问权限不同,C++中可以使用访问说明符来管理类成员的访问权限以达到更好的封装。如果使用struct,则在第一个访问说明符前的成员默认为public,class则相反。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class person
{
public: //以下成员在整个程序中都可以被访问
person()=default;
person(string s1, string s2) {name=s1;addr=s2;}
person(person &item) {copy(item);}
person(istream &in) {read(in,*this);}
private: //以下成员只可以被类的成员函数访问
string name;
string addr;
person& copy(const person &x)
{
name=x.name;
return *this;
}
}

需要注意,private说明符后的成员只可以被类的成员函数访问,像前面讲到的read和print函数不属于类的成员函数,因此无法访问private中的成员。

友元

若想非类成员函数访问private内容,可以在类中使用friend关键字对其进行友元声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class person
{
friend istream &read(istream &in, person &item);
friend ostream &print(ostream &out, person &item);
}

istream &read(istream &in, person &item)
{
in>>item.name>>item.addr;
return in;
}
ostream &print(ostream &out, person &item)
{
out<<"NAME: "<<item.name<<endl;
out<<"ADDR: "<<item.addr<<endl;
return out;
}

友元声明也适用于声明其他类及其成员函数

1
2
3
4
5
6
7
8
9
class person
{
friend class test;
friend void test::test1();
}
class test
{
void test1(){...};
}

关于类的静态成员

在仔细阅读了书籍过后个人觉得用处不是特别大,如果以后用到了相关知识再在这里补充,目前就不写了。