【C++】vector的底层封装和实现
目录
- 目录
- 前言
- 基本框架
- 迭代器
- 容量
- 第一个测试,野指针异常
- 第二轮测试,浅拷贝的问题
- 元素访问
- 修改操作
- push_back
- insert
- 迭代器失效问题
- erase
- 默认成员函数
- 构造函数
- 双重构造引发调用歧义
- 拷贝构造
- 赋值重载
- 析构函数
- 源码
- end
目录
前言
废话不多说,我们直接来模拟实现vector的底层实现,有了上篇文章string的底层封装和模拟实现,相信大家已经对stl的容器的底层有了一定的了解
基本框架
namespace bit {template<class T>class vector {public:typedef T* iterator;typedef const T* const_iterator;// 主要接口函数private:iterator _start = nullptr;iterator _finish = nullptr;iterator _end_of_storage = nullptr;};
}
- 这里我们的三个成员变量参考源码中的实现方式,用指针来模拟实现。
- 并且采用缺省参数的形式将它们都初始化为nullptr,这样当我们后面在写 构造函数和析构函数 的时候就不需要再去做初始化了
迭代器
- 这里就很简单的返回我们指向开头的指针和指向最后一个元素的指针就行了
iterator begin()
{return _start;
}iterator end()
{return _finish;
}const_iterator begin() const
{return _start;
}const_iterator end() const
{return _finish;
}
容量
- 如何获取元素个数和内存容量,在C语言中我们讲过指针减去指针就是他们之间的元素个数,如此即可
size_t size()
{return _finish - _start;
}size_t capacity()
{return _end_of_storage - _start;
}
- 这里我们来重点说一下这个reserve扩容函数,他牵扯到一个很大的问题。

- 我们这里再写一个push_back的接口(后面讲),让代码先跑起来
void push_back(const T& x)
{if (_finish == _end_of_storage){size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;reserve(newCapacity);}*_finish = x;_finish++;
}
第一个测试,野指针异常
- 测试代码
bit::vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
v.push_back(4);
结果:

- 可以看到,程序崩溃了,那是什么原因了,不妨调试看一下

- 插入的时候对空指针解引用,我们再一步步看看_finish指针为啥是空指针
_finish = _start + size();
- 这个时候就知道了,我们扩容的时候_start已经更新到tmp新空间的位置,但是更新_finish的时候,size()函数的结构是这样的

- 但是现在我们的_start已经是更新到tmp新空间的位置上去了,这时候_finish是旧空间,两个指针不是同一块空间相减就是一个随机值

- 那么又如何来解决这个问题呢?
- 根据上面的描述,我们发现其实就是因为更新_finish我们调用了size函数造成的问题,那么这个时候我们其实就是想要知道元素个数,但是必须要在_start更新之前,所以我们就在_start更新之前获取元素个数即可
if (n > capacity())
{// 先保存一下原先的size()size_t sz = size();T* tmp = new T[n]; // 开一块新空间if (_start){memcpy(tmp, _start, sizeof(T) * size());delete[] _start;}_start = tmp;_finish = _start + sz;_end_of_storage = _start + n;
}
第二轮测试,浅拷贝的问题
- 下面是我们要进行第二轮测试的代码,内部类型使用的是 string类
void test_vector2()
{bit::vector<string> v;v.push_back("11111");v.push_back("22222");v.push_back("33333");v.push_back("44444");for (auto e : v){cout << e << " ";}cout << endl;
}
- 运行起来看并没有什么问题

- 但是呢当我再去push_back(“55555”)的时候程序却出现了问题,这个时候发生了扩容

- 就是对于自定义类型,虽然我们的确是tmp独立开了一块新空间,但是memcpy函数就是把指针_str的值拷贝过来,导致原来的_str和tmp的_str指向同一块空间。就会发生两个指针指向同一块空间。
- 那么在执行下面这句代码的时候,先会去把this指针的_str指向的那块空间析构掉,这个时候因为他们指向的是同一块空间,改变_str也同时更改了tmp中的_str
delete[] _start;

- 那么知道了是什么问题,我们又该如何解决这个问题呢?
- 我们知道导致这个原因的就是因为memcpy是浅拷贝,那么我们实现一个深拷贝就行了
这里我们就直接执行下面的代码:
for (size_t i = 0; i < OldSize; i++)
{tmp[i] = _start[i];
}
- 这里的逻辑就是把_start的每个值赋值给tmp用来更新新的_start,这个时候就要重载一下赋值重载完成深拷贝,这会在后面介绍,请往下看。
接下来再看一下resize这个函数的实现
void resize(size_t n, const T& val = T())
{if (n <= size()){_finish = _start + n;}else{reserve(n);while (_finish < _start + n){*_finish = val;_finish++;} }
}
- 如果重置的长度小于原来的长度,那么直接把_finish更新到_start + n的位置,就是n个长度就行
- 如果大于原来的长度,直接扩容到n个,如果扩容到n个后_finish还是小于n个的时候,再把_finish更新到n个。
元素访问
- 对于元素访问的话我们最常用的就是下标 + []的形式,这里给出两种,一个是const版本和非const版本
T& operator[](size_t pos)
{assert(pos < size());return _start[pos];
}T& operator[](size_t pos) const
{assert(pos < size());return _start[pos];
}
修改操作
push_back
void push_back(const T& x)
{if (_finish == _endofstorage){reserve(capacity() == 0 ? 4 : 2 * capacity());}*_finish = x;++_finish;
}
- 这一块的逻辑非常简单,当最后一个元素的指针和容量的指针位置相同的时候,就需要扩容,否则直接插入,更新_finish指针就行了
insert
void insert(iterator pos, const T& x)
{assert(pos >= _start && pos <= _finish);// 1.首先考虑扩容逻辑if (_finish == _end_of_storage){size_t newCapacity = capacity() == 0 ? 4 : capacity() * 2;reserve(newCapacity);}// 2.挪动数据iterator end = _finish - 1;while (end >= pos){*(end + 1) = *end;--end;}*pos = x;++_finish;
}
- 这一块同理,插入数据先考虑是否需要扩容
- 然后把pos位置后面的数据往后面挪,知道pos位置插入x即可。
迭代器失效问题
1.这里指定位置插入函数,如果空间不管扩容的时候就出现了迭代器失效的问题
2.如果不更新pos的位置,那我们还在释放的旧空间里面插入,就会发生访问野指针!

- 那么如何去解决这个问题呢?
首先我们要明确导致这个问题发生的就是pos没有正确的更新,pos需要更新到新空间和原空间的相应位置,那么我们就算出旧空间中pos的相对位置,在新空间开辟后,把pos更新到新空间的相对位置就行了
- 但是呢就上面这样还不够,我们只解决了内部迭代器失效的问题,而外部迭代器失效的问题并没有很好地解决。
- 外部迭代器又是个什么东西,接下来看看这段代码
bit::vector<int>::iterator it = v.begin();
v.insert(it, 33);
bit::print(v);cout << *it << endl;bit::print(v);
- 可以看到,在使用完这个这个迭代器之后再去访问就出现了问题

- 这是因为,形参迭代器的传值并不能改变外面的实参迭代器。
- 有的同学就会说,那简单,传个引用就行了,但是你再来看看这个情况
v.insert(v.begin() + 3, 6);


erase
- 对于【erase】来说,我们也是需要先去挪动数据的,但是在这里呢我们需要从前往后挪,也是防止造成覆盖的情况
void erase(iterator pos)
{assert(pos >= _start && pos < _finish);iterator end = pos + 1;// 移动覆盖while (end != _finish){*(end - 1) = *end;++end;}--_finish;
}
- 立马来测试一下

- 看起来并没啥问题,那接下来看看下面的2个场景
- 场景1:我们在中间插入两个偶数,现在我们要对容器中的偶数进行删除,可以看看下方结果


- 可以看到发生了逻辑错误,我们的目的是把所有的偶数都删除了,可是现在容器中还有偶数!
Why?
如下图,我们删除一个数后,会把他前面的一个数挪到被删除数字的位置,那么这个时候it++,如果被挪到到删除位置的数是一个偶数就会漏掉这个数的判断。

- 场景2:如果最后一个元素是偶数,那么删除会咋样呢?如图看看我们的代码,那这样把最后一个元素删除后,不就越界访问了吗,it 会永远不等于 _finish

- 这个时候有人说,我们就要修改一下,当删除偶数的时候,不要让it++,这是对的,但还缺点东西

- 可是,万一别人底层实现erase很多次,容器的数据变少了,别人要缩容呢? 缩容是异地缩容,就是重新开一段比之前小的空间,把原来的空间数据拷贝过去,最后释放原来的空间,异地缩容是因为C++开辟内存只能是连续的内存,所以释放的时候不能只释放一部分。这时候it不就又变成,inset中的那样野指针的问题导致迭代器失效了吗?

- 这个时候,就返回被删除元素的下一个元素的位置值,这里返回pos,是因为我们的算法是被删除元素的下一个位置元素移到这个被删除元素的位置,所以被删除元素的下一个元素的位置就是pos本身.当然这里我们没有实现缩容,但是如果这里缩容,就会更新pos的位置,把新pos返回的

默认成员函数
构造函数
- 首先的话一定是构造函数,有参构造是一定要实现的,因为这里的逻辑和resize()是类似的,因此我们直接去做一个复用即可
// 有参构造
vector(size_t n, const T& val = T())
{resize(n, val);
}
- 那有同学可能会问,三个私有成员变量不需要去做初始化吗?
- 这个时候我们的缺省值就发挥作用了,很好的避免了忘记初始化三个成员变量的问题
private:iterator _start = nullptr;iterator _finish = nullptr;iterator _end_of_storage = nullptr;
- 除了上面这种初始化,我再介绍一种方法:那就是使用 迭代器区间
// [first, last)
template<class InputIterator>
vector(InputIterator first, InputIterator last)
{while (first != last){push_back(*first);++first;}
}
- 复用push_back接口就行了
- 我们再补充一个构造
vector(initializer_list<T> lt):_start(nullptr),_finish(nullptr),_endofstorage(nullptr){//initializer_list底层就是自己开了一块空间,类似数组这种,那么他就支持迭代器for (auto e : lt){push_back(e);}}
- 这个考前使用迭代器就行,他底层是类似数组
双重构造引发调用歧义

- 那么如何解决呢?我们模板匹配还有个原则就是有现成吃现成的,如果有int类型的构造,模板就不用去实例化,所以重载一个int类型的,让他匹配

拷贝构造
vector(const vector<T>& v)
{reserve(v.capacity()); //和被拷贝的对象开一样大的空间,防止一直扩容,提高效率for (auto& e : v){push_back(e);}
}
- push_back扩容的时候完成深拷贝
赋值重载
vector<T>& operator=(vector<T> v)
{swap(v);return *this;
}
- 这里的逻辑类似string里面的,this想要得到v的资源并且把原来的资源抛弃,得到v的资源直接交换他们指向的指针就行了。由于v是局部变量,调用完会析构,就把this的资源析构了。

析构函数
~vector()
{if (_start){delete[] _start; //同一块空间_start = _finish = _endofstorage = nullptr;}
}
源码
#pragma once
#include<iostream>
#include<vector>
#include<assert.h>
using namespace std;
namespace bit
{template<class T>class vector{public:typedef T* iterator;typedef const T* const_iterator;vector():_start(nullptr),_finish(nullptr),_endofstorage(nullptr){}vector(initializer_list<T> lt):_start(nullptr),_finish(nullptr),_endofstorage(nullptr){//initializer_list底层就是自己开了一块空间,类似数组这种,那么他就支持迭代器for (auto e : lt){push_back(e);}}vector(const vector<T>& v){reserve(v.capacity()); //和被拷贝的对象开一样大的空间,防止一直扩容,提高效率for (auto& e : v){push_back(e);}}template <class InputIterator> //类模板的成员函数也可以是一个函数模板, 这里的迭代器可以是任何类型如:string, list的迭代器构造vector(InputIterator first, InputIterator last) {while (first != last){push_back(*first);first++;}}vector(size_t n, const T& val = T()){reserve(n);for (size_t i = 0; i < n; i++){push_back(val);}}vector(int n, const T& val = T()){reserve(n);for (int i = 0; i < n; i++){push_back(val);}}void swap(vector<T>& tmp){std::swap(_start, tmp._start);std::swap(_finish, tmp._finish);std::swap(_endofstorage, tmp._endofstorage);}vector<T>& operator=(vector<T> v){swap(v);return *this;}~vector(){if (_start){delete[] _start; //同一块空间_start = _finish = _endofstorage = nullptr;}}iterator begin(){return _start;}iterator end(){return _finish;}const_iterator begin() const{return _start;}const_iterator end() const{return _finish;}T& operator[](size_t i){assert(i < size());return _start[i];}const T& operator[](size_t i) const{assert(i < size());return _start[i];}size_t size() const{return _finish - _start;}size_t capacity() const{return _endofstorage - _start;}void reserve(size_t n){size_t OldSize = size();if (n > capacity()){T* tmp = new T[n];for (size_t i = 0; i < OldSize; i++){tmp[i] = _start[i];}delete[] _start;_start = tmp;_finish = _start + OldSize;_endofstorage = _start + n;}}void push_back(const T& x){if (_finish == _endofstorage){reserve(capacity() == 0 ? 4 : 2 * capacity());}*_finish = x;++_finish;}bool empty() const{return size() == 0;}void pop_back(){assert(!empty()); --_finish;}void resize(size_t n, const T& val = T()){if (n <= size()){_finish = _start + n;}else{reserve(n);while (_finish < _start + n){*_finish = val;_finish++;} }}iterator insert(iterator pos, const T& x) //这里改成引用我希望形参可以改变实参{assert(pos >= _start && pos <= _finish);if (_finish == _endofstorage){size_t len = pos - _start;reserve(capacity() == 0 ? 4 : 2 * capacity());pos = _start + len;}iterator end = _finish;while (end != pos){*end = *(end - 1);end--;}*pos = x;_finish++;return pos;}iterator erase(iterator pos){assert(pos >= _start && pos < _finish);iterator i = pos + 1;while (i < _finish){*(i - 1) = *i;i++;}_finish--;return pos;}private:iterator _start = nullptr; //写一个类一定要把缺省参数写上,防止初始化列表没初始化导致的一系列越权访问问题iterator _finish = nullptr;iterator _endofstorage = nullptr;};
}
end
感谢阅读,希望对大家有所帮助。快去实现一下吧
相关文章:
【C++】vector的底层封装和实现
目录 目录前言基本框架迭代器容量第一个测试,野指针异常第二轮测试,浅拷贝的问题 元素访问修改操作push_backinsert迭代器失效问题 erase 默认成员函数构造函数双重构造引发调用歧义 拷贝构造赋值重载析构函数 源码end 目录 前言 废话不多说࿰…...
Open CASCADE学习|读取点集拟合样条曲线(续)
问题 上一篇文章已经实现了样条曲线拟合,但是仍存在问题,Tolerance过大拟合成直线了,Tolerance过大头尾波浪形。 正确改进方案 1️⃣ 核心参数优化 通过调整以下参数控制曲线平滑度: Standard_Integer DegMin 3; // 最低阶…...
ARM Cortex-M用于控制中断和异常处理的寄存器:BASEPRI、PRIMASK 和 FAULTMASK
在ARM Cortex-M处理器中,BASEPRI、PRIMASK 和 FAULTMASK 是用于控制中断和异常处理的系统级寄存器。它们的主要区别在于作用范围和灵活性,以下是详细说明: 1. PRIMASK • 功能: 禁用除以下情况的异常和所有中断(Maska…...
Kafka 中的生产者分区策略
Kafka 中的 生产者分区策略 是决定消息如何分配到不同分区的机制。这个策略对 Kafka 的性能、负载均衡、消息顺序性等有重要影响。了解它对于高效地使用 Kafka 进行消息生产和消费至关重要。 让我们一起来看 Kafka 中 生产者的分区策略,它如何工作,以及…...
【Django】教程-11-ajax弹窗实现增删改查
【Django】教程-1-安装创建项目目录结构介绍 【Django】教程-2-前端-目录结构介绍 【Django】教程-3-数据库相关介绍 【Django】教程-4-一个增删改查的Demo 【Django】教程-5-ModelForm增删改查规则校验【正则钩子函数】 【Django】教程-6-搜索框-条件查询前后端 【Django】教程…...
结构化需求分析:专业方法论与实践
结构化需求分析是一种用于软件开发或其他项目中的系统分析方法,旨在全面、准确地理解和描述用户对系统的需求。以下是关于结构化需求分析的详细介绍: 一、概念 结构化需求分析是采用自顶向下、逐步分解的方式,将复杂的系统需求分解为若干个…...
R语言:气象水文领域的数据分析与绘图利器
R 语言是一门由统计学家开发的用于统计计算和作图的语言(a Statistic Language developed for Statistic by Statistician),由 S 语言发展而来,以统计分析功能见长。R 软件是一款集成 了数据操作、统计和可视化功能的优秀的开源软…...
Kotlin与HttpClient编写视频爬虫
想用Apache HttpClient库和Kotlin语言写一个视频爬虫。首先,我需要确定用户的具体需求。视频爬虫通常涉及发送HTTP请求,解析网页内容,提取视频链接,然后下载视频。可能需要处理不同的网站结构,甚至可能需要处理动态加载…...
图形化编程语言:低代码赛道的技术革命与范式突破
在 2024 年 Gartner 低代码平台魔力象限报告中,传统低代码厂商市场份额增速放缓至 12%,而图形化编程语言赛道融资额同比激增 370%。本文深度剖析低代码平台的技术瓶颈,系统阐释图形化编程语言的核心优势,揭示其如何重构软件开发范…...
蓝桥杯每日刷题c++
目录 P9240 [蓝桥杯 2023 省 B] 冶炼金属 - 洛谷 (luogu.com.cn) P8748 [蓝桥杯 2021 省 B] 时间显示 - 洛谷 (luogu.com.cn) P10900 [蓝桥杯 2024 省 C] 数字诗意 - 洛谷 (luogu.com.cn) P10424 [蓝桥杯 2024 省 B] 好数 - 洛谷 (luogu.com.cn) P8754 [蓝桥杯 2021 省 AB2…...
快速上手示例(以BEVFormer为例)
快速上手示例(以BEVFormer为例) 安装依赖: bash git clone https://github.com/fundamentalvision/BEVFormer.git cd BEVFormer pip install -r requirements.txt下载预训练模型: wget https://github.com/fundament…...
GitHub 上开源一个小项目的完整指南
GitHub 上开源一个小项目的完整指南 🚀 第一步:准备你的项目 在开源之前,确保项目是可用且有一定结构的: ✅ 最低要求 项目文件清晰、结构合理(比如:src/、README.md、LICENSE)项目能在本地正…...
当实体类中的属性名和表中的字段名不一样 ,怎么办
在不同的持久化框架中,当实体类中的属性名和表中的字段名不一致时,有不同的解决办法,下面为你详细介绍: 1. MyBatis MyBatis 是一个流行的持久层框架,有两种主要方式来处理属性名和字段名不一致的情况。 方式一&…...
arthas之dump/classloader命令的使用
文章目录 1. dump2. classloader 1. dump 作用:将已加载类的字节码文件保存到特定目录:logs/arthas/classdump/ 参数 数名称参数说明class-pattern类名表达式匹配[c:]类所属 ClassLoader 的 hashcode[E]开启正则表达式匹配,默认为通配符匹…...
linux 使用 usermod 授权 普通用户 属组权限
之前写过这篇文章 linux 普通用户 使用 docker 只不过是使用 root 用户编辑 /etc/group用户所属组文件的方式 今天带来一种 usermod 命令行方式 以下3步,在root用户下操作 第一步,先创建一个普通用户测试使用 useradd miniuser第二步,授权到…...
大文件上传之断点续传实现方案与原理详解
一、实现原理 文件分块:将大文件切割为固定大小的块(如5MB) 进度记录:持久化存储已上传分块信息 续传能力:上传中断后根据记录继续上传未完成块 块校验机制:通过哈希值验证块完整性 合并策略:所…...
第一次3D打印,一个简单的小方块(Rhino)
一、建模 打开犀牛,我们选择立方体 我们点击上册的中心点 输入0,然后回车0 而后我们输长度:10,回车确认 同样的,宽度10 高度同样是10 回车确认后,我们得到一个正方形 二、导出模型 我们选择文件—>保存…...
java基础使用- 泛型
泛型 泛型作用泛型语法(1) 泛型类/接口(2) 泛型方法 类型参数命名习惯类型通配符(Wildcards)(1) 无界通配符 <?>表示“未知类型”(2) 上界通配符 <? extends T>表示“T 或 T 的子类”。(3) 下界通配符 <? super T>表示“T 或 T 的父…...
VMware-workstation-full-12.5.2 install OS X 10.11.1(15B42).cdr
手把手虚拟机安装苹果操作系统 VMware_workstation_full_12.5.2 unlocker208 Apple Max OS X(M)-CSDN博客 vcpu-0:VERIFY vmcore/vmm/main/physMem_monitor.c:1180 FILE: FileCreateDirectoryRetry: Non-retriable error encountered (C:\ProgramData\VMware): Cann…...
5分钟上手GitHub Copilot:AI编程助手实战指南
引言 近年来,AI编程工具逐渐成为开发者提升效率的利器。GitHub Copilot作为由GitHub和OpenAI联合推出的智能代码补全工具,能够根据上下文自动生成代码片段。本文将手把手教你如何快速安装、配置Copilot,并通过实际案例展示其强大功能。 一、…...
deepseek使用记录26——从体力异化到脑力异化
我们的一切发现和进步,似乎结果是使物质力量具有理智生命,而人的生命则化为愚钝的物质力量。AI快速发展的现实中,人面临着比工业革命更深刻的异化。在工业革命中,人的身躯沦为了机器的一部分,而现在人的脑袋沦为了AI的…...
数字身份DID协议:如何用Solidity编写去中心化身份合约
本文提出基于以太坊的自主主权身份(SSI)实现方案,通过扩展ERC-734/ERC-735标准构建链上身份核心合约,支持可验证声明、多密钥轮换、属性隐私保护等特性。设计的三层架构体系将身份控制逻辑与数据存储分离,在测试网环境…...
【Git “ls-tree“ 命令详解】
本章目录: 1. 命令简介2. 命令的基本语法和用法基本语法常见使用场景示例 1:查看当前提交的文件树示例 2:查看某个分支的文件树示例 3:查看特定路径下的文件树 3. 命令的常用选项及参数常用选项: 4. 命令的执行示例示例 1…...
[ctfshow web入门] web16
信息收集 提示:对于测试用的探针,使用完毕后要及时删除,可能会造成信息泄露 试试url/phpinfo.php url/phpsysinfo.php url/tz.php tz.php能用 点击phpinfo,查看phpinfo信息,搜索flag,发现flag被保存为变量…...
全面支持MCP协议,开启便捷连接之旅,MaxKB知识库问答系统v1.10.3 LTS版本发布
2025年4月7日,MaxKB开源知识库问答系统正式发布v1.10.3 LTS版本。 在MaxKB v1.10.3 LTS版本中,应用方面,MaxKB新增支持MCP调用节点,AI对话节点新增MCP工具调用功能,支持设置MCP服务配置;函数库方面&#x…...
ES:geoip_databases
目录 如何查看 .geoip_databases 的内容1. 查看 .geoip_databases 的内容2. 查看GeoIP数据库的统计信息3. 使用GeoIP处理器4. 管理GeoIP数据库更新 如何查看 .geoip_databases 的内容 在Elasticsearch中,.geoip_databases 是一个特殊的索引,用于存储Geo…...
VTK知识学习(51)- 交互与Widget(二)
1、交互器样式 前面所讲的观察者/命令模式是 VTK实现交互的方式之一。在前面示例 所示的窗口中可以使用鼠标与柱体进行交互,比如用鼠标滚轮可以对柱体放大、缩小;按下鼠标左键不放,然后移动鼠标,可以转动柱体;按下鼠标左键,同时按…...
底盘---麦克纳姆轮(Mecanum Wheel)
一、基本定义与起源 定义:麦克纳姆轮是一种实现全向移动的特殊轮式结构,通过在主轮周边安装多个倾斜的辊子(小轮),使设备能够在平面上向任意方向移动(包括横向、斜向、旋转等),无需…...
深入源码级别看spring bean创建过程
我们通常聊到spring bean的生命周期,大多是从网上找帖子背些基本概念,这样我们学到的东西是不够直观清晰的,这篇文章我就试着从源码级别来讲清楚bean的创建过程。 一、准备demo代码 我们既然要深入源码来看bean的创建过程,那么就…...
I/O进程1
day1 一、标准IO 1.概念 在C库中定义的一组用于输入输出的函数 2.特点 (1).通过缓冲机制减少系统调用,提高效率 (2.)围绕流进行操作,流用FILE *来描述(3).标准IO默认打开了三个流,stdin(标准输入)、stdout(…...
