栈和队列和优先级队列
本节目标
- stack的介绍和使用
- queue的介绍和使用
- priority_queue的介绍和使用
- 容器适配器
stack的介绍和使用
- stack是一种容器适配器,专门用在具有后进先出操作的上下文环境中,其删除只能从容器的一端进行元素的插入与提取操作。
- stack是作为容器适配器被实现的,容器适配器即是对特定类封装作为其底层的容器,并提供一组特定的成员函数来访问其元素,将特定类作为其底层的,元素特定容器的尾部(即栈顶)被压入和弹出。
- stack的底层容器可以是任何标准的容器类模板或者一些其他特定的容器类,这些容器类应该支持以下操作:
- empty:判空操作
- back:获取尾部元素操作
- push_back:尾部插入元素操作
- pop_back:尾部删除元素操作
- 标准容器vector、deque、list均符合这些需求,默认情况下,如果没有为stack指定特定的底层容器,默认情况下使用deque
- 栈的这种数据结构是不能用来进行遍历的操作的
#include<iostream>
#include<stack>
#include<queue>
using namespace std;
void TestStack()
{
stack<int> s;
s.push(1);
s.push(2);
s.push(3);
s.push(4);
cout << s.size() << endl;
cout << s.top() << endl;
s.pop();
s.pop();
cout << s.size() << endl;
cout << s.top() << endl;
if (s.empty())
{
cout << "栈已经空了" << endl;
}
else
{
cout << "栈中还存在有元素" << endl;
}
}
int main()
{
TestStack();
return 0;
}
最小栈
class MinStack {
public:
//现在需要实现一个最小栈的操作
stack<int> s;
stack<int> ret;
/** initialize your data structure here. */
MinStack()
{
}
void push(int x)
{
s.push(x);
if(ret.empty()||x<=ret.top())
ret.push(x);
}
void pop()
{
if(s.top()==ret.top())
ret.pop();
s.pop();
}
int top()
{
return s.top();
}
int getMin()
{
return ret.top();
}
};
/**
* Your MinStack object will be instantiated and called as such:
* MinStack* obj = new MinStack();
* obj->push(x);
* obj->pop();
* int param_3 = obj->top();
* int param_4 = obj->getMin();
*/
验证栈序列
class Solution {
public:
bool validateStackSequences(vector<int>& pushed, vector<int>& popped)
{
//验证栈队列
if(pushed.size()==0&&popped.size()==0)
return true;
if(pushed.size()==0||popped.size()==0)
return false;
stack<int> ret;
int j=0;
for(auto c : pushed)
{
ret.push(c);
while(!ret.empty()&&j<popped.size()&&ret.top()==popped[j])
{
ret.pop();
++j;
}
}
return ret.empty();
}
};
逆波兰表达式
class Solution {
public:
int evalRPN(vector<string>& tokens)
{
stack<int> ret;
for(const string&s : tokens)
{
if(s=="+"||s=="-"||s=="*"||s=="/")
{
int a=ret.top();
ret.pop();
int b=ret.top();
ret.pop();
if(s=="+")
ret.push(b+a);
else if(s=="-")
ret.push(b-a);
else if(s=="*")
ret.push(b*a);
else if(s=="/")
ret.push(b/a);
}
else
{
ret.push(atoi(s.c_str()));
}
}
return ret.top();
}
};
stack的模拟实现
#pragma once
#include<vector>
namespace bite //给命名空间是为了防止产生冲突的
{
template<class T>
class stack
{
public:
stack()
{
//构造函数
}
void push(const T& data)
{
_vStack.push_back(data); //直接调用栈中尾插的操作
//如果容量不够的话,也不用担心,因为vector是会进行自动扩容的操作的
}
void pop()
{
if (_vStack.empty())
return;
return _vStack.pop_back(); //直接调用尾删的方式来对你进进行删除的操作
}
T& top()
{
return _vStack.back();
}
const T& top()const
{
return _vStack.back();
}
size_t size()const
{
return _vStack.size();
}
bool empty()const
{
return _vStack.empty();
}
private:
std::vector<int> _vStack; //把数据放在vStack中进行保存的操作
};
}
#include<iostream>
#include<stack>
#include<queue>
using namespace std;
//void TestStack()
//{
// stack<int> s;
//
// s.push(1);
// s.push(2);
// s.push(3);
// s.push(4);
//
// cout << s.size() << endl;
// cout << s.top() << endl;
//
// s.pop();
// s.pop();
//
// cout << s.size() << endl;
// cout << s.top() << endl;
//
// if (s.empty())
// {
// cout << "栈已经空了" << endl;
// }
// else
// {
// cout << "栈中还存在有元素" << endl;
// }
//}
#include"Stack.h"
void TestStack2()
{
bite::stack<int> s;
s.push(1);
s.push(2);
s.push(3);
s.push(4);
cout << s.size() << endl;
cout << s.top() << endl;
s.pop();
s.pop();
cout << s.size() << endl;
cout << s.top() << endl;
if (s.empty())
{
cout << "栈已经空了" << endl;
}
else
{
cout << "栈中还存在有元素" << endl;
}
}
int main()
{
TestStack2();
return 0;
}
queue
- 队列是一种容器适配器,专门用于在FIFO上下文(先进先出)中操作,其中从容器一端插入元素,另一端
提取元素。 - 队列作为容器适配器实现,容器适配器即将特定容器类封装作为其底层容器类,queue提供一组特定的
成员函数来访问其元素。元素从队尾入队列,从队头出队列。 - 底层容器可以是标准容器类模板之一,也可以是其他专门设计的容器类。该底层容器应至少支持以下操
作: - empty:检测队列是否为空
- size:返回队列中有效元素的个数
- front:返回队头元素的引用*
- back:返回队尾元素的引用
- push_back:在队列尾部入队列
- pop_front:在队列头部出队列
queue的模拟实现
#pragma once
#include<list>
namespace bite
{
template<class T>
class queue
{
public:
queue()
{
//构造函数,什么都不用做
}
void push(const T& data)
{
q.push_back(data);
}
void pop()
{
if (q.empty())
return;
q.pop_front(); //相当于是头删的操作
}
T& front()
{
return q.front();
}
const T& front()const
{
return q.front();
}
T& back()
{
return q.back();
}
const T& back()const
{
return q.back();
}
size_t size()const
{
return q.size();
}
bool empty()const
{
return list.empty();
}
private:
std::list<T> q;
};
}
#include<iostream>
#include<stack>
#include<queue>
using namespace std;
void TestQueue()
{
queue<int> q;
q.push(1);
q.push(2);
q.push(3);
q.push(4);
q.push(5);
q.push(6);
cout << q.front() << endl;
cout << q.back() << endl;
cout << q.size() << endl;
q.pop();
q.pop();
cout << q.front() << endl;
cout << q.back() << endl;
cout << q.size() << endl;
}
#include"Queue.h"
void TestQueue2()
{
bite::queue<int> q;
q.push(1);
q.push(2);
q.push(3);
q.push(4);
q.push(5);
q.push(6);
cout << q.front() << endl;
cout << q.back() << endl;
cout << q.size() << endl;
q.pop();
q.pop();
cout << q.front() << endl;
cout << q.back() << endl;
cout << q.size() << endl;
}
int main()
{
TestQueue();
TestQueue2();
return 0;
}
优先级队列
- 优先级队列默认使用vector作为其底层存储数据的容器,在vector上又使用了堆算法将vector中元素构造成堆的结构,因此priority_queue就是堆,所有需要用到堆的位置,都可以考虑使用priority_queue。注意:默认情况下priority_queue是大堆
- 优先级队列实际上就是堆,其底层就是将堆进行的重新封装
- 优先级队列和队列用的是同一个头文件
- priority_queue() / priority_queue(first,last)—构造一个空的优先级队列
- empty( )
- 检测优先级队列是否为空,是返回true,否则返回false
- top( ) 返回优先级队列中最大(最小元素),即堆顶元素
- push(x) 在优先级队列中插入元素x
- pop() 删除优先级队列中最大(最小)元素,即堆顶元素
- 这些操作中,其实需要注意的一点有top这个操作,top这个操作只能去访问堆顶的元素,不可以对堆顶的元素进行修改的操作
- 如果现在需要修改堆顶的元素的话,那儿么我们只能对其进行删除,然后再进行插入新元素的操作,通过这些操作,我们来改变堆顶的元素,注意注意top是一定不可以改变的
#include<queue>
#include<functional>
using namespace std;
int main()
{
//默认是使用小于的方式来进行比较的
//priority_queue<int> q;
//如果我们希望他用大于的方式来进行比较的话,我们就需要给成如下的形式
priority_queue<int, vector<int>, greater<int>> q;
//greate再functional这个头文件里面
q.push(3);
q.push(1);
q.push(6);
q.push(0);
q.push(9);
q.push(4);
q.push(2);
q.push(8);
cout << q.size() << endl;
cout << q.top() << endl;
q.pop();
cout << q.size() << endl;
cout << q.top() << endl;
return 0;
}
优先级队列针对内置类型
#include<queue>
#include<functional>
using namespace std;
//首先针对内置类型的元素
void TestPriorityQueue1()
{
//如果你现在就是要去创建大堆的话,那么你对下面的这句话完全可以不用去进行任何修改的操作
//那么我如何去验证优先级队列他其实就是一个大堆呢?
//很好理解,我们可以给这个堆中去随机的插入一些元素,然后看一看结果就ok了
priority_queue<int> p;
p.push(4);
p.push(2);
p.push(9);
p.push(7);
p.push(1);
p.push(8);
p.push(3);
p.push(0);
p.push(2);
p.push(6);
cout << p.size() << endl;
cout << p.top() << endl; //top只是将元素获取到了,并没有将元素进行删除的操作
p.pop();
p.pop();
cout << p.size() << endl;
cout << p.top() << endl; //top只是将元素获取到了,并没有将元素进行删除的操作
//如果你现在希望去创建一个小堆的话,你只需要把比较的方式给出来就可以了
//priority_queue<int,vector<int>,greater<int>> p;
//p.push(4);
//p.push(2);
//p.push(9);
//p.push(7);
//p.push(1);
//p.push(8);
//p.push(3);
//p.push(0);
//p.push(2);
//p.push(6);
//cout << p.size() << endl;
//cout << p.top() << endl; //top只是将元素获取到了,并没有将元素进行删除的操作
//p.pop();
//p.pop();
//cout << p.size() << endl;
//cout << p.top() << endl; //top只是将元素获取到了,并没有将元素进行删除的操作
}
优先级队列针对自定义类型
- 如果我们直接向相面这样子来写代码的话,其实我们的代码是会崩溃的
- 崩溃的原因在于使用默认的比较方式是不可以的,之所以内置类型可以通过编译原因在于,内置类型之间是可以直接进行比大小的操作的,而自定义类型之间是不可以直接进行比大小的操作的,所以,如果像下面这样写代码的话,肯定是会发生崩溃的问题的
- 其实,这种错误也是很容易解决的,既然Date日期类没有提供比较大小的重载的方式,那么我们自己吧日期类之间比大小的方式给出来就好了,这样一来,就可以正常的进行日期类之间的比大小了,从而代码就不会发生崩溃的错误了
#include<queue>
#include<functional>
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
}
private:
int _year;
int _month;
int _day;
};
void TestPriorityQueue3()
{
Date d1(2020, 5, 28);
Date d2(2020, 5, 27);
Date d3(2020, 5, 29);
priority_queue<Date> p1;
p1.push(d1);
p1.push(d2);
p1.push(d3);
}
int main()
{
//TestPriorityQueue1();
TestPriorityQueue3();
return 0;
}
- 当我们像下面这样给出重载的方式之后,代码就可以正常的通过编译了
#include<queue>
#include<functional>
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
}
bool operator<(const Date& d)const
{
//我们在这里给出一个简单的操作,就只是去比较天数的大小
//从而来确定两个日期的大小
return _day < d._day;
}
private:
int _year;
int _month;
int _day;
};
void TestPriorityQueue3()
{
Date d1(2020, 5, 28);
Date d2(2020, 5, 27);
Date d3(2020, 5, 29);
priority_queue<Date> p1;
p1.push(d1);
p1.push(d2);
p1.push(d3);
}
int main()
{
//TestPriorityQueue1();
TestPriorityQueue3();
return 0;
}
- 出了我们自己给出大于的方式和小于的方式,我们还可以通过仿函数的方式来对上面啊的问题进行修改
- 通过函数指针的方式给出比较的方式
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
}
//bool operator<(const Date& d)const
//{
// //我们在这里给出一个简单的操作,就只是去比较天数的大小
// //从而来确定两个日期的大小
// return _day < d._day;
//}
bool operator>(const Date& d)const
{
//我们在这里给出一个简单的操作,就只是去比较天数的大小
//从而来确定两个日期的大小
return _day > d._day;
}
friend bool IsLess(const Date& left, const Date& right);
private:
int _year;
int _month;
int _day;
};
bool IsLess(const Date& left, const Date& right)
{
//但是采用这种方式的话,有一个问题,就是,如果类变成私有的话
//那么这种方式其实就是不能再继续去进行使用了
return left._day < right._day;
//使用这种方式传递过去函数指针其实就是可以的
}
//定义一个函数指针出来
typedef bool (*Less)(const Date& left, const Date& right);
void TestPriorityQueue3()
{
Date d1(2020, 5, 28);
Date d2(2020, 5, 27);
Date d3(2020, 5, 29);
priority_queue<Date,vector<Date>, Less>p1(IsLess);
p1.push(d1);
p1.push(d2);
p1.push(d3);
}
int main()
{
//TestPriorityQueue1();
TestPriorityQueue3();
return 0;
}
- 该函数指针的类型(Less)一定要出现在模板参数列表中
- 当然,我们还有另外的方式,我们可以使用仿函数或者说是函数对象的方式去进行比较的方式
- 仿函数和函数对象是可以像函数调用一样使用的对象
#include<iostream>
using namespace std;
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
}
//bool operator<(const Date& d)const
//{
// //我们在这里给出一个简单的操作,就只是去比较天数的大小
// //从而来确定两个日期的大小
// return _day < d._day;
//}
bool operator>(const Date& d)const
{
//我们在这里给出一个简单的操作,就只是去比较天数的大小
//从而来确定两个日期的大小
return _day > d._day;
}
friend bool IsLess(const Date& left, const Date& right);
int GetDay()const
{
return _day;
}
private:
int _year;
int _month;
int _day;
};
bool IsLess(const Date& left, const Date& right)
{
//但是采用这种方式的话,有一个问题,就是,如果类变成私有的话
//那么这种方式其实就是不能再继续去进行使用了
return left._day < right._day;
//使用这种方式传递过去函数指针其实就是可以的
}
//定义一个函数指针出来
typedef bool (*Less)(const Date& left, const Date& right);
class Greater
{
public:
bool operator()(const Date& left, const Date& right)
{
//我们按照大于的方式来进行比较
return left.GetDay() > right.GetDay();
}
};
void TestPriorityQueue3()
{
Date d1(2020, 5, 28);
Date d2(2020, 5, 27);
Date d3(2020, 5, 29);
//然后我现在去创建一个Greater类型的对象
Greater gt;
//这个时候gt并不是一个函数,所以如果直接这样调用的话,代码显然是会出现错误的
//如果希望向下面这样写的话,首先需要保证gt是一个对象,我们才可以像这样子来进行调用的操作
//那么如果你现在就非要像这样进行调用的话
//那么你首先需要确保gt是一个函数才可以
//那么,我们只需要去在greater类中对()这种操作符进行重载的操作其实就是可以的了
//gt.operator()(d1, d2); //通过对象去调用重载的操作
gt(d1, d2); //gt本质上是一个对象,但是他使用的方式和函数的方式是相同的,所以说把这种类型的对象称为函数对象
//也可以将其称为仿函数
if (gt(d1, d2)) //如果d1大于d2的话
{
cout << "d1>d2" << endl;
}
else
{
cout << "d1<=d2" << endl;
}
priority_queue<Date,vector<Date>, Less>p1(IsLess);
p1.push(d1);
p1.push(d2);
p1.push(d3);
}
int main()
{
//TestPriorityQueue1();
TestPriorityQueue3();
return 0;
}
优先级队列的top-K 问题
优先级队列的模拟实现
#pragma once
#include<vector>
#include<functional>
namespace bite //防止饿标准库中的优先级队列发生冲突
{
template<class T, class Container = std::vector<T>, class Cmp = std::less<T>>
class priority_queue
{
public:
priority_queue(const Cmp& cmp = Cmp(), const Container& con = Container()) //如果没有提供我就给出默认的
:_con(con)
, _cmp(cmp)
//进行初始化
{
//为了调高效率,我们给出类类型的引用
}
//当然我们也可以按照区间的方式来进行构造
template<class Iterator>
priority_queue(Iterator first, Iterator last, const Cmp& cmp = Cmp(), const Container& con = Container())
:_con(con)
,_cmp(cmp)
{
//然后我们现在要把数据插入优先级队列里面去
//直接使用vector中对于一块连续空间的插入的方式
_con.insert(_con.begin(), _con.end());
//然后接下来需要对con的元素进行调整,让它满足堆的特性
//然后我们现在需要进向下的调整
int root = (_con.size() - 2 )>> 1;
for (; root >= 0; root--)
{
_AdjustDown(root);
}
}
void push(const T& data)
{
_con.push_back(data);
_AdjustUp(_con.size() - 1);
}
void pop()
{
if (empty())
{
return;
}
swap(_con[0], _con[_con.size() - 1]);
_con.pop_back();
_AdjustDown(0);
}
const T& top()const
{
return _con[0];
}
size_t size()const
{
return _con.size();
}
bool empty()
{
return _con.empty();
}
//既然container现在已经是一种类型了,那么我们就可以利用这种类型去创建容器了
//这个容器就是来存储优先级队列里面的元素的
private:
void _AdjustDown(int parent)
{
//child来标记两个孩子中较大的那一个
int child = parent * 2 + 1;
while (child<_con.size())
{
if (child + 1 < _con.size() && _cmp(_con[child + 1] , _con[child]))
{
child += 1;
}
if (_cmp(_con[child] , _con[parent]))
{
swap(_con[child], _con[parent]);
parent = child;
child = parent * 2 + 1;
}
else
return;
}
}
void _AdjustUp(int child)
{
//child来标记两个孩子中较大的那一个
int parent = (child - 1) >> 1;
while (child)
{
if (_cmp(_con[child] , _con[parent]))
{
swap(_con[child], _con[parent]);
child = parent;
parent = (child - 1) >> 1;
}
else
return;
}
}
private:
Container _con;
//然后我们再给出要给比较器的方法出来就可了
Cmp _cmp;
};
}
#include"Priority_Queue.h"
void TestBitePriority_Queue()
{
bite::priority_queue<int> q;
q.push(4);
q.push(2);
q.push(9);
q.push(7);
q.push(1);
q.push(8);
q.push(3);
q.push(0);
q.push(2);
q.push(6);
cout << q.size() << endl;
cout << q.top() << endl;
q.pop();
q.pop();
cout << q.size() << endl;
cout << q.top() << endl;
}
int main()
{
TestBitePriority_Queue();
return 0;
}
容器适配器
- 适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结),该模式是将一个类的接口转换成客户希望的另外一个接口。
- 为什么会被称给容器适配器,因为这种容器里面是可以存放数据的
- 举个例子:
- 除了优先级队列是适配器以外,栈和队列其实也是适配器
- 所以,在一般情况下,我们也把栈和队列称为容器适配器,而没有将其划分在容器的行列里面,因为栈和队列底层都是对双端队列进行的封装
- 所以,栈,队列,优先级队列都属于容器适配器
deque的简单介绍
deque的原理介绍
- deque(双端队列):是一种双开口的"连续"空间的数据结构,双开口的含义是:可以在头尾两端进行插入和
删除操作,且时间复杂度为O(1),与vector比较,头插效率高,不需要搬移元素;与list比较,空间利用率比
较高。 - 在头部进行删除和删除和在尾部进行插入和删除的时间复杂度都是O(1)
- deque并不是真正连续的空间,而是由一段段连续的小空间拼接而成的,实际deque类似于一个动态的二维
数组,既然是动态的二维数组的话,那么其实就相当于每一行的空间都是动态开辟出来的,那么每一行的地址都需要保存起来,不然的话,就会造成内存泄漏的后果,那么我们还需要给出一个空间,这个空间用来保存每一行的地址
- 双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问的假象,落
在了deque的迭代器身上
- 还需要一段空间把每一个绿色的空间记录下来,那么其实就是下图的map
- 那么,如果此时map的空间不够用了的话, 那么我们就去开辟一块更大的内存空间,然后将map拷贝过去其实就是可以的了,然后将原有的map空间释放了,就行
- 因为在及进行拷贝的时候,只是拷贝的是每一行的地址,并不需要真正的去拷贝一个一个得元素,所以减少了元素的拷贝
- deque的迭代器,迭代器里面总共维护了四个指针,cur指向的是我们当前正在访问的元素,first指向该段空间的起始位置,last指向的是该段空间的末尾位置,当cur==last的时候我就知道了我现在已经处在空间末尾的位置了,既然已经处于空间末尾了,那么我就应该去取下一段空间了,那么我如何去取下一段空间的地址呢?我们现在还有一个node,node指向的值之前所说的map,map里面存储的是每行空间的首地址,cur可能的等于first,也可能等于last,要看数据的走向,然后去判断node的走向
- 所以我们其实可以看出来,deque不适合做便利的操作,因为,在遍历期间,需要不断地进行判断,判断迭代器cur是否在该段空间的起始位置或者说是否在该段空间的末尾位置,如果不在的话,则使用迭代器继续进行遍历的操作,如果cur已经在空间的起始位置或者说,在空间的末尾位置了,我们就需要通过迭代器node在map中找下一块空间,所以其实就相当于对于每一段空间来说,我们都需要进行多一次的遍历,所以说deque不适合来进行迭代遍历的操作
deque的缺陷
- 与vector比较,deque的优势是:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不
需要搬移大量的元素,因此其效率是必vector高的。 - 与list比较,其底层是连续空间,空间利用率比较高,不需要存储额外字段。
- 但是,deque有一个致命缺陷:不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下,而序列式场景中,可能需要经常遍历,因此在实际中,需要线性结构时,大多数情况下优先考虑vector和list,deque的应用并不多,而目前能看到的一个应用就是,STL用其作
为stack和queue的底层数据结构。
为什么选择deque作为stack和queue的底层默认容器
- stack是一种后进先出的特殊线性数据结构,因此只要具有push_back()和pop_back()操作的线性结构,都可
以作为stack的底层容器,比如vector和list都可以;queue是先进先出的特殊线性数据结构,只要具有
push_back和pop_front操作的线性结构,都可以作为queue的底层容器,比如list。但是STL中对stack和
queue默认选择deque作为其底层容器,主要是因为: - stack和queue不需要遍历(因此stack和queue没有迭代器),只需要在固定的一端或者两端进行操作。
- 在stack中元素增长时,deque比vector的效率高(扩容时不需要搬移大量数据);queue中的元素增长
时,deque不仅效率高,而且内存使用率高。结合了deque的优点,而完美的避开了其缺陷。(因为queue如果底层使用的是list的话,那么其实list是一个双向链表,那么在存储的时候,底层还需要保存他的prev指针和next指针,空间就会使用的更加多一些,list底层是一个一个的节点,这些一个一个的结点都是通过空间配置器申请出来的,那么申请新空间其实也是需要耗费时间的)
C++中的I/O流
- C语言的输入与输出
- 流是什么
- C++IO流
- stringstream的简单介绍
C语言的输入与输出
- C语言中我们用到的最频繁的输入输出方式就是scanf ()与printf()。 scanf(): 从标准输入设备(键盘)读取数
据,并将值存放在变量中。printf(): 将指定的文字/字符串输出到标准输出设备(屏幕)。注意宽度输出和精度
输出控制。C语言借助了相应的缓冲区来进行输入与输出 - 如何写一个不定餐的Add函数
#include<iostream>
using namespace std;
int main()
{
//为什么C语言中已经有了一套自己的输入输出的方式
//C++中还要给出自己的输入输出流呢?
//原因就在于C语言里面的输入输出在有些情况下我们使用起来其实并不是那么的方便
int a = 0;
//C语言的输入,首先在我需要输入的时候,我需要知道我输入的这个数据他到底是什么类型的数据
//要给出格式控制,不用类型的数,格式控制都是不一样的
scanf("%d", &a);
printf("%d", a);
//正是因为C语言中的输入输出有很多的缺陷
//所以C++才又给出了一套自己的I/0流
//所以重新搞出了自己的一套输入输出的方式
//提供给用户使用
return 0;
}
流是什么(I/O流)
- “流”即是流动的意思,是物质从一处向另一处流动的过程,是对一种有序连续且具有方向性的数据( 其单位
可以是bit,byte,packet )的抽象描述。 C++流是指信息从外部输入设备(如键盘)向计算机内部(如内存)
输入和从内存向外部输出设备(显示器)输出的过程。这种输入输出的过程被形象的比喻为“流”。 - 它的特性是:有序连续、具有方向性
- 为了实现这种流动,C++定义了I/O标准类库,这些每个类都称为流/流类,用以完成某方面的功能
C++IO流
- C++系统实现了一个庞大的类库,其中ios为基类,其他类都是直接或间接派生自ios类
- 点到cout,然后转到定义进行查看
为什么C++中可以进行直接的输入和输出的操作
- 因为其已经对基本的所有类型进行了重载的操作了,所以也就不需要像C语言中在进行输出的时候还要给出格式控制了
- 将所有的内置类型都已经进行了重载的操作了
- 只是针对于自定义类型的时候,需要对这个操作符进行重载的操作
三种输入输出的方式
- cerr和clog标准错误输出流,输出设备是显示器在流类库中,最重要的两部分功能为标准输入/输出(standard input/output)和文件处理。在C++的流类库中定义了四个全局流对象:cin,cout,cerr和clog:cin标准输入流对象,键盘为其对应的标准设备;cout标准输出流对象,显示器为标准设备。在新库中要使用这四个功能,必须包含文件并引入std标准命名空间
注意
- cin为缓冲流。键盘输入的数据保存在缓冲区中,当要提取时,是从缓冲区中拿。如果一次输入过多,会留在那儿慢慢用,如果输入错了,必须在回车之前修改,如果回车键按下就无法挽回了。只有把输入缓冲区中的数据取完后,才要求输入新的数据。不可能用刷新来清除缓冲区,所以不能输错,也不能多输
- 输入的数据类型必须与要提取的数据类型一致,否则出错。出错只是在流的状态字state中对应位置位(置1),程序继续。
- 空格和回车都可以作为数据之间的分格符,所以多个数据可以在一行输入,也可以分行输入。但如果是字符型和字符串,则空格(ASCII码为32)无法用cin输入,字符串中也不能有空格。回车符也无法读入
// 单个元素循环输入
while(cin>>a)
{
// ...
}
// 多个元素循环输入
while(cin>>a>>b>>c)
{
// ...
}
// 整行接收
while(cin>>str)
{
// ...
}
文件分类
- C++根据文件内容的数据格式分为二进制文件和文本文件
- 可以更容易的角度去理解,如果你现在打开一个文件,你是可以看的懂这个文件的,那么这个文件它其实就是文本文件,如果你打开一个文件,你发现,你不能直观的看懂他所表达的意思,那么它其实就是二进制文件了
- 使用feof来检测二进制文件,feof是一个函数
//I/O流
/*
gets是可以用来接受字符串中的空格的,gets也是优缺点的
他需要用户来提前把空间给好,这个其实也是C语言中针对字符串操作的一个弊端
scanf函数的返回值成功接受输入的个数
scanf和printf都是不定参数的
printf的返回值是成功输出字符的个数
C++ 中的I/O流
cin进行输入的操作,cout输出,cerr标准错误输出 clog打印日志信息
在c++中,后面三个是没有任何区别的,因为都是属于ostream的
都是ostream的全局对象
只不过是三者的使用场景是有一些区别的
cout不需要格式控制,非常方便,之所以不用进行格式控制
是因为ostream已经把各种各样的格式进行了重载,也就是重载过了
所有的内置类型都已经重载过了,也就是所有的内置类型都已经处理过了
所以就可以直接进行输出的操作了
istream是一样的道理
cin在进行输入的时候,需要注意cin是以空格和回车作为间隔的
如果想要处理空格和回车,可以使用gets和getline
c++中的文件IO流
fopen,fread,fwrite,fgets,fget...
*/