文章目录
- 📖 前言
- 1. IO流
- 1.1 C语言的输入和输出:
- 1.2 流的概念及特性:
- 1.3 自定义类型隐式类型转换:
- 1.4 在线OJ中的输入和输出:
- 1.5 C++IO流对文件的操作:
- 1.6 stringstream介绍:
- 2. 空间配置器
- 2.1 什么是空间配置器:
- 2.2 为什么需要空间配置器:
- 2.3 STL空间配置器实现原理:
- 2.3 - 1 内存池
- 2.3 - 2 SGI-STL中二级空间配置器设计
- 2.3 - 3 如何分配内存
- 2.4 空间配置器在STL中的应用:
- 3. 总结
- 4. 赠言
📖 前言
本章讲简单的介绍C++的IO流和空间配置器,并学习一下C++STL库中空间配置器的底层结构,本章只是简单的了解学习,并不要求深入学习……🙌 🙌 🙌
1. IO流
1.1 C语言的输入和输出:
在我们刚学C语言的时候,我们就知道了输入和输出分别是scanf
和printf
。
- scanf: 从标准输入设备(键盘)读取数据,并将值存放在变量中。
- printf: 将指定的文字/字符串输出到标准输出设备(屏幕),注意宽度输出和精度输出控制。
C语言是格式化的输入和输出:
- scan是扫描的意思,我们可以理解成输入
- print是打印的意思,我们可以理解成输出
- f - 是format的缩写,是格式的意思
所以我们在用C语言输入和输出的时候,要指定格式,例如以整形的格式输入读取,以整形的格式输出,以字符串的形式输入输出……
C语言借助了相应的缓冲区来进行输入与输出,如下图所示:
1.2 流的概念及特性:
概念:
流: 即是流动的意思,是物质从一处向另一处流动的过程,是对一种有序连续且具有方向性的数据( 其单位可以是bit,byte,packet )的抽象描述。
C++的流:
- C++流是指信息从外部输入设备(如键盘)向计算机内部(如内存)输入和从内存向外部输出设备(显示器)输出的过程。
- 这种输入输出的过程被形象的比喻为 “流”。
流的特性:
- 有序连续、具有方向性
为了实现这种流动,C++定义了I/O标准类库,这些每个类都称为流/流类,用以完成某方面的功能。
C++库中标准IO流:
- C++系统实现了一个庞大的类库,其中ios为基类,其他类都是直接或间接派生自ios类。
- 他们之间是继承的关系
- 值得一提的是这里C++标准库中罕见使用了菱形继承。😀😀😀
既然有继承,那么这里就可以玩多态……
C++标准库提供了4个全局流对象cin、cout、cerr、clog,使用cout进行标准输出,即数据从内存流向控制台(显示器)。
int main()
{
cout << "hello" << endl;
cerr << "hello" << endl;
clog << "hello" << endl;
return 0;
}
这里结果都是hello。
补充:
使用cin进行标准输入即数据通过键盘输入到程序中,同时C++标准库还提供了cerr用来进行标准错误的输出,以及clog进行日志的输出,从上图可以看出,cout、cerr、clog是ostream类的三个不同的对象,因此这三个对象现在基本没有区别,只是应用场景不同,在使用时候必须要包含文件并引入std标准命名空间。
cin和cout可以直接输入和输出内置类型数据,原因:标准库已经将所有内置类型的输入和输出全部重载了。
istream重载:
ostream重载:
1.3 自定义类型隐式类型转换:
在我们之前的学习中,我们知道:括号()这个运算符,可以做函数调用的参数列表,也可以是控制优先级的,也可以是强制类型转换。
如果一个类要强转成其他类型,而我们之前学习的仿函数已经将括号()给重载了,所以我们要是想重载强制类型转换,就不能再用operator ()
了,C++只能退而求其次搞了一种新的玩法。
见下述代码:
class Date
{
friend ostream& operator << (ostream& out, const Date& d);
friend istream& operator >> (istream& in, Date& d);
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
//支持Date对象转换成bool
//重载运算符,但是怎么重载类型了呢?
//这里支持的就是将自定义类型转换成内置类型
operator bool()
{
//这里是随意写的,假设输入_year为0,则结束
if (_year < 1)
return false;
else
return true;
}
//转换成整形
operator int()
{
return _year + _month + _day;
}
//括号()这个运算符,可以做函数调用的参数列表,也可以是控制优先级的,也可以是强制类型转换
//仿函数已经用了operator()了,那么强制类型转换就不能用operator()转换了
private:
int _year;
int _month;
int _day;
};
这里支持的就是将自定义类型转换成内置类型。
注意:普通的operator后面接的都是运算符,而这里operator后面接的却是一个类型。
int main()
{
Date d1 = -1;
Date d2 = { 2023, 3, 23 };
//自定义类型转换成内置类型
bool ret1 = d1;
bool ret2 = d2;
//反过来转
int i1 = d1;
int i2 = d2;
cout << ret1 << endl;
cout << ret2 << endl;
//支持转成bool,本质是调用了重载函数
if (d1)
{
}
return 0;
}
这里调用和运算符重载时一样,直接调用对应的operator 类型,这里就实现了自定义类型转成内置类型。
就可以实现,内置类型的对象也能放在if
或者while
的判断中进行逻辑判断。
1.4 在线OJ中的输入和输出:
在我们之前的刷题中,一定遇到过多组输入的测试题目,C语言通常采用如下的方式:
我们知道scanf
函数的返回值是一个整形,返回的是读取成功数据的个数,读取失败或者是读完之后会返回回个EOF
。
EOF: 是end of file
的缩写,其值是-1,-1的补码全是1,取反则全是0,这就是第二种判断的原理
而C++中则是以下面的这种方式来写:
而我们之前在学习日期类中自己实现对日期类的输入和输出时,我们知道,ci
对象重载之后返回的还是一个cin
的对象。
istream& operator >> (istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
ostream& operator << (ostream& out, const Date& d)
{
out << d._year << " " << d._month << " " << d._day;
return out;
}
cin
对象的内部肯定是支持重载成内置类型,所以可以将cin >> str
,这种写法。
1.5 C++IO流对文件的操作:
两段程序仅供参考,这里了解性学习即可,毕竟C语言中我们详细学了文件的操作,到现在也没怎么用过嘛~😁😁😁
int main()
{
//是读文件
//in是读取,out才是写文件
ifstream ifs("text.cpp");
//重载了operator bool
//正常读取返回true,读取失败或者读到文件结尾了就返回false
//while (ifs)
//{
// //一个字符一个字符的读,读出来之后打印
// char ch = ifs.get();
// cout << ch;
//}
//自动把空格忽略掉了
char ch;
while (ifs >> ch)
{
cout << ch;
}
return 0;
}
第一个循环可以直接将文件原封不动的读完,第二个循环则是会自动忽略掉空格~
从一个文件读到另一个文件当中:
int main()
{
ifstream ifs("text.cpp");
ofstream ofs("Copy.cpp");
char ch;
ch = ifs.get();
//while (ifs)
while (~ch)
{
//插入到文件当中去
ofs << ch;
cout << ch;
ch = ifs.get();
}
return 0;
}
1.6 stringstream介绍:
序列化和反序列化:
- 序列化就是将数据转化成字符串,反序列化就是将字符串转化成对应的数据类型
在C语言中,我们若是想要将一个整型变量的数据转化为字符串格式,有以下方法:
- 使用sprint函数进行转化
int main()
{
int a = 10;
char arr[10];
sprintf(arr, "%d", a);
cout << arr << endl;
return 0;
}
将整型的a转化为字符串格式存储在字符串arr当中。
C++中的stringstream:
int main()
{
//把整形转成字符串,相当于将整形插入进去就转成字符串了
int i = 123;
double d = 44.55;
ostringstream oss;
oss << i;
cout << oss.str() << endl;
string stri = oss.str();
//赋值覆盖一下
oss.str("");
oss << d;
string strd = oss.str();
oss.str("");
Date d1(2022, 10, 11);
oss << d1;
string strdt = oss.str();
cout << strdt << endl;
istringstream iss(strdt);
Date d2;
iss >> d2;
return 0;
}
stringstream 用来转换自定义类型还是有优势,可以很方便的将整形转字符串,字符串转整形。
综上总结:
- istream 和 ostream 底层本质是在做类型转换,转成字符串存起来
- 然后存储的字符串的去向不同
2. 空间配置器
2.1 什么是空间配置器:
空间配置器,顾名思义就是为各个容器高效的管理空间(空间的申请与回收)的,在默默地工作。
2.2 为什么需要空间配置器:
前面在模拟实现vector、list、map、unordered_map等容器时,所有需要空间的地方都是通过new申请的,虽然代码可以正常运行,但是有以下不足之处:
- 空间申请与释放需要用户自己管理,容易造成内存泄漏
- 频繁向系统申请小块内存块,容易造成内存碎片
- 频繁向系统申请小块内存,影响程序运行效率
- 直接使用malloc与new进行申请,每块空间前有额外空间浪费
- 申请空间失败怎么应对
- 代码结构比较混乱,代码复用率不高
- 未考虑线程安全问题
所以我们需要设计一块高效的内存管理机制。
2.3 STL空间配置器实现原理:
- 我们这里学习的是SGI版本的STL空间配置器的结构:
SGI-STL
以12
8作为小块内存与大块内存的分界线- 将空间配置器其分为两级结构
- 一级空间配置器处理大块内存,二级空间配置器处理小块内存
2.3 - 1 内存池
内存池就是: 先申请一块比较大的内存块已做备用,当需要内存时,直接到内存池中去去,当池中空间不够时,再向内存中去取,当用户不用时,直接还回内存池即可。避免了频繁向系统申请小块内存所造成的效率低、内存碎片以及额外浪费的问题。
首先我们先储备知识,我们需要空间需要向堆申请,malloc
也是个内存池,它提供服务的对象是整个程序,内空间配置器则是针对STL容器服务的。同时不同版本的malloc
实现的方式也是不一样的,malloc每次开空间大小也并不是要的大小,会多一点,比如空间前面会记录这块空间多大,留释放用。
关系图:
空间配置器就是单独开一块大的空间,留给申请小空间的用,就不用频繁的到堆上取,造成内存碎片化。
2.3 - 2 SGI-STL中二级空间配置器设计
当申请的空间大于128 Byte
的时候就不会走内存池,而是直接用malloc
去直接申请,当申请的内存小于128 Byte
的时候则会走内存池申请。
- SGI-STL中的二级空间配置器使用了内存池技术
-
- 但没有采用链表的方式对用户已经归还的空间进行管理
-
- 因为用户申请空间时在查找合适的小块内存时效率比较低
-
- 而是采用了哈希桶的方式进行管理。
- 那是否需要128桶个空间来管理用户已经归还的内存块呢?
-
- 答案是不需要,因为用户申请的空间基本都是4的整数倍,其他大小的空间几乎很少用到。
-
- 因此:SGI-STL将用户申请的内存块向上对齐到了8的整数倍
- 内存还回来还要继续给别人用,哪块内存符合再来申请空间的大小呢?要挨个挨个遍历,效率低了,为了方便管理,运用到了哈希桶。
示意图:
挨个挨个用链表挂起来,那待会再来申请内存的时候优先再这取,每个桶中挂的是内存块,取内存的时候直接在对应的桶中取就好。
- 128个桶的话,分的太细不好,内存太碎了,所以在这里给了八字节对齐这种玩法。
- 0号桶挂的是8Byte,1号桶挂的是16Byte…
- 内存大小 == (访问的桶 + 1)* 8
2.3 - 3 如何分配内存
如果申请的空间大于128Byte的话,直接走一级空间配置器,调用malloc函数,如果是小于128Byte的话,则是走二级空间配置器。
- 先到哈希桶找,再去内存池找,再去malloc找(此时才申请内存池),所有申请内存都先来找哈希桶。
每次给的内存的大小:
- 每次申请的内存块大小不一定是桶中正好有的内存块的大小
- 内存块挂起来管理,按一定规则对齐, 就会导致内碎片
-
- 此时我们就会多给一点,给比申请的内存稍微大一点的内存
-
- 例如,我们需要7个字节,就会给8个字节,需要12个字节,就会给16个字节,需要14个字节,也会给16个字节。
- 空间配置器,只会多给,不会克扣
- 这里最多浪费7字节,当然桶越多浪费就越少,因为内存被分的越碎
- 如果桶为空,则向内存池中申请一大块内存,然后第一个留下有用,剩下的分为同样的所需大小,挂在桶中。
- 下次再来找相同的大小快的内存,就不用再去找内存池了,而是直接在对应的桶中头删一个。
- 如果内存池用完了就再去
malloc
,只要系统有内存就可以申请。 - 内存还回来以后,可以再次用而且申请空间的效率非常的高小于
128Byte
直接到桶里取内存就可以了。
- 如果对应的哈希桶没有内存了,内存池也没了,堆上的空间也用完了,它就会到其他的桶中去看有无内存,例如:会将
60Byte
的前40Byte
切出来留用,后20Byte
挂起来。
优点:
- 用哈希桶的好处是,要多大的内存可以很快的找到对应的链表,要是释放也能很快的找到对应的链表。
- 时间复杂度:申请内存是O(1),释放内存也是O(1),因为是哈希桶的头删和头插。
- 内存还回来以后可以再次用而且申请空间的效率非常的高,小于128Bvte直接到桶里取内存就可以了。
- 内存块两用的,给分配的用,或者记录下个内存块地址,链接用把自身管理起来。
- 在头四个或者是
八个Byte
存下一个内存的地址就可以了最小的内存块是4字节,不存在存不下下一个内存块地址的问题,因为无论在哪个平台下,指针的大小要么是4Byte
,要么是8个Byte
。
补充:
一个进程里面只有一 个空间配置器也就是说空间配置器可以设置成单例模式库里的实现和单例一样都能保证只有一对象。
2.4 空间配置器在STL中的应用:
我们以list为例:
3. 总结
本章内容只需了解,要知道其底层大概的结构,明白其思想即可。
4. 赠言
至此,C++和数据结构的学习暂时告一段落,感谢一路相伴,我们Linux再见~~😊😊😊👋👋👋
少年所要走的路才不会有孤独,因为每一步都有属于自己的使命与归途。往事暗沉不可追,来日之路光明灿烂。