8月18日复习了模板和流的相关内容。
模板,简单地说就是一个“多功能”的程序模块。这个模块的输入,不限定某一个类型。
具体来说,对于模板函数,传入参数的数据类型可以不固定,但逻辑结构要相同。模板函数说明可以写为:
template <typename 类型形参>;
返回类型(可以是类型形参) 函数名(类型形参 参数名A,类型形参 参数名B);
参数A和B可以是不固定的类型。调用时如果使用:
cin>>a>>b;
cout<<函数名(a,b);
那么在调用这个函数时,编译器将调用参数类型为输入的a和b的数据类型的重载版本的函数。这个过程也就是实例化。这样一来,就可以避免在对数据类型不同的操作对象进行逻辑相同的操作时,程序员还需要再写一个仅仅参数类型不同的函数的问题了。
函数模板实例化时,如果进行替换的实际参数有多个,他们类型不相同,却要进行加减、比较等操作,模板机制不会提供参数类型自动转换。也就是说:
template <typename a>;
a compare(const a x,const a y) {return x>y?x:y;}
如果有参数int x和char y,那么调用函数模板在实例化时将会报错“参数类型无法匹配”。这时需要人为的进行一个重载:
int compare(const int x,const char y);
那么编译器可以进行隐式类型转换,这时再调用函数,无论是char类型在前还是int类型在前,都就可以正常执行了。
除了有函数模板,还有类模板。在定义类前说明一个类属类型,用这个类型去定义类中的一些函数成员或者数据成员。类模板的实例化和函数模板的实例化类似,都是赋予一个具体的数据类型。类模板可以作为函数参数,也可以在作为派生类从类模板派生或普通类派生。模板类也可以从类模板派生或普通类派生。从类模板派生模板类的例子:
#include <iostream>
using namespace std;
template <typename a>
class Base //定义基类:类模板
{
public:
Base(a x) {xx = x;}
void out() { cout << xx << endl; }
protected:
a xx;
};
class First :public Base<int> //派生模板类
{
public:
First(int y, double yy) :Base<int>(y) { k=yy; }
void out() { Base<int>::out();cout << k << endl; }
protected:
double k;
};
int main()
{
Base<int> data1(3);
data1.out();
First data2(1, 2);
data2.out();
}
标准模板库STL主要组件有容器、迭代器和算法。容器是数据结构:
序列容器:vector向量、deque双向队列、list双向链表
关联容器:set集合、multiset集合((允许重复值元素)、map映射、multimap映射(允许重复值元素)
容器适配器:stack堆栈(LIFO)、queue队列(FIFO)、pirority_queue优先队列
对容器有一些共同的操作:
Container c(beg,end) 以beg地址为起点,到地址end的数据序列作为初值构造容器
c.size() 返回容器元素个数
c.begin() c.end() 顾名思义
c.insert(pos,elem) 在迭代器pos所指位置前插入elem
c.clear() 清空容器
还有一些序列容器特有的操作:
c.front() /c.back() 访问容器首/尾元素数据
c.push_back(elem)/c.pop_back() 插入/删除尾元素数据
c[i] 访问第i个元素
下面是一个STL容器操作的例子:
#include <iostream>
#include <list>
#include <cstdlib>
#include <ctime>
using namespace std;
//建立有序链表
void creatList(list<int>& orderList, int Len)
{
int i, k;
for (i = 0;i < Len;i++)
{
k = rand() % 100; //生成多个随机数
orderList.push_back(k); //插入容器末尾
}
orderList.sort(); //排序
};
//输出链表
void outList(list<int>& List)
{
list<int>::iterator p; //建立迭代子p
p = List.begin();
while (p != List.end())
{
cout << *p << " ";
p++;
}
cout << "\r\n";
};
//合并链表
void inorderMerge(list<int>& A1, list<int> A2)
{
A1.merge(A2);
};
int main()
{
list<int> A1, A2;
srand(int(time(0))); //初始化随机序列起点
creatList(A1, 10);
outList(A1);
cout << endl;
creatList(A2, 5);
outList(A2);
cout << endl;
inorderMerge(A1, A2);
outList(A1);
}
迭代子好比一个可以指向容器内元素的指针,可以通过c.begin() c.end() 指向首尾。迭代子也有着类似指针的操作,*p是其所指向的对象。STL库定义了五种迭代器,输入、输出,正向、双向、随机访问。迭代器在头文件iterator中声明,但一般容器头文件已经包含,不用特地去声明。
STL库还包含了多种算法,常有的有find查找、find_if条件查找、sort排序等等。算法在头文件algorithm中声明。力扣的题凡是用到数组的地方,大多都会用vector向量代替,并调用STL库的操作。大概是因为在减少工作量的同时并不会显著升高算法复杂度。毕竟c++是效率之王。
最后一章,c++中的流。记得刚转专业后补修c++时,因为那个学期课程又多又难,学习c++也是囫囵吞枣,写代码只知道一开始就#include<iostream>,再来一个using namespace std;经常自己写完这两句,后面的写不下去了,就去CSDN用crtl+c大法,真是说来惭愧。久而久之,就这两句打起来最熟练,但打了这么多次,却不知道这两句是什么意思。iostream的头文件里包含了哪些东西?std又是一个什么样的命名空间?都没有去深究。
今天复习到这里,终于解开了当时的疑问。“流”指的是数据流,玩过单片机用过串口就知道,数据在主从机是怎么进行交互的。流主要指的是输入流和输出流,输入流是从键盘鼠标等设备流向内存,输出流是从内存流向显示器、打印机等等,这都是通过字节流实现,说的再直白些,就是一串八个八个的由“0”或“1”组成的比特流。
c++的流类库是用继承的方法建立起的一个类库,有两个平行的基类streambuf类和ios类。带buf的都跟缓存有关,ios类库则提供了高级的I/O操作。ios派生了两个类,就是比较熟悉的istream类和ostream类。他俩共同派生了iostream类,就是那个我经常无脑包含的头文件。iostream类也有三个派生类,是文件输入输出fstream类、串输入输出strstream类和标准输入输出stdiostream类。
常用的头文件除了iostream,还有iomanip,它包含了格式化I/O的带参数操纵算子,用于指定输入输出格式。头文件fstream处理文件有关信息。
标准流cin,cout,cerr和clog,前两个很熟悉,后两个是错误输出流,不能重定向。其中,输入流提取运算符>>有类型转换功能,可以把输入的空格、Tab转换成分隔符。而istream中一些函数可以实现特殊的提取功能,如get可以从流中提取字符包括空格,这也是为什么检测输入空格时经常用get。getline从流中提取一行字符;read无格式输入指定字节数。 输出流提供了成员函数put和write,可以在输出中插入字节或是字节序列。
输入输出流中有着格式控制的功能。ios提供了如dec(输入/输出转化为十进制)、showpoint(输出时显示小数点)等等格式控制标志常量,可以给用户提供方便的格式控制。iomanip中的格式控制符有我们常用的setw(控制输出宽度),还有setprecision(设置浮点数输出精度)等等。
串流可以连接string对象和字符串。使用串流类要包含sstream头文件,可以从string对象直接提取/插入数据。文件流可以进行对文件的开关读写,要包含fstream头文件,文件流类提供了直接操作文件的函数。虽然封装程度不比python,但也为用户提供了巨大便利。