STL 是 Standard Template Library 的缩写,中文译为“标准模板库”。STL 是 C++ 标准库的一部分。
我们之前已经基本了解了C++中的模板templet
,以及模板的作用。可以说,C++STL就是一套功能强大的 C++ 模板类,提供了通用的模板类和函数,这些模板类和函数可以实现多种流行和常用的算法和数据结构,如向量、链表、队列、栈等。并且做到了数据结构和算法的分离(使用模板可以将一种算法的实现不局限于一种数据结构)。
C++ STL的核心主要包括以下三种组件:
容器(Containers)
容器是用来管理某一类对象的集合。C++ 提供了各种不同类型的容器,比如 deque、list、vector、map 等。不同的容器基于不同的数据结构
算法(Algorithms)
算法作用于容器。它们提供了执行各种操作的方式,包括对容器内容执行初始化、排序、搜索和转换等操作。以此同时,多亏了C++Templet,算法的实现也独立于容器。
迭代器(iterators)
迭代器用于遍历对象集合的元素。这些集合可能是容器,也可能是容器的子集。
除了上面所述的STL的三大核心以外,C++STL中的算法(Algorithms)还有一个特点,那就是,算法中某一步骤的实现方式可以通过于用户外部传参(传入一个自定义的函数)实现,大大增加了算法的多样性。
举一个例子,相信大家都有用过Algorithms中的sort()排序算法,sort算法的最后一个参数由用户传入比较函数,sort()算法进而根据用户自定义的比较方式进行排序。这样一来我们就可以要求sort算法按从小到大或是从大到小的方式进行排序。除此之外,假如我们传入一个类,只要我们在比较函数中定义对这个类的排序是按照类中的哪个成员按照哪种方式进行比较,sort函数就可以理解我们的意图。而不需要重复定义多个sort算法。
接下来我将定义自己的算法和函数,结合容器和迭代器解决序列变换(如取反、平方、立方),像素变换(二值化、灰度拉伸)。
自定义操作函数(可以类比于sort()):
// 对于顺序表而言的运算(使用自定义运算函数 MyOperator)
template <class T, class MyOperator>
void transCalc(T a, T& b, int nNum, MyOperator op)
{
for(int i=0;i<nNum;i++)
{
b[i] = op(a[i]);
}
}
// 对于链表而言的运算(使用自定义运算函数 MyOperator)
template <class inputIt, class outputIt, class MyOperator>
void transCalcT(inputIt begInput, inputIt endInput, outputIt begOutput, MyOperator op)
{
for(;begInput!=endInput;begInput++,begOutput++){
*begOutput = op(*begInput);
}
}
自定义操作模板(可以类比于用户自定义的比较函数):
// 这里定义操作模板,可以自定义op操作:
//取反
template<class T>
T InvT(T a)
{
return -a;
}
//平方
template<class T>
T SqrT(T a)
{
return a*a;
}
// 类操作模板,二值化:
// 由于二值化除了传入变量本身还需要传入阈值,因此使用类来定义
template<class T>
class MyThreshold
{
public:
int threshold;
// n默认是128
MyThreshold(int n=128):threshold(n){
}
// 重载操作符"()",一旦使用()传入参数就执行自定义内容:
int operator()(T val)
{
return val > threshold;
}
};
//比较模板函数
template<class T>
bool MyCompare(T a, T b)
{
return a > b;
}
//自定义比较模板类
template<class T>
class MyComp
{
public:
int op;
// 自定义比较
MyComp(int n):op(n){
}
bool operator()(T a, T b)
{
switch(op){
case 0:
return a == b;
break;
case 1:
return a > b;
break;
case -1:
return a < b;
break;
}
}
};
打印函数,方便可视化:
// 打印函数
template <class T>
void outputCont(string strName, T beg, T end)
{
cout<<strName;
for(;beg!=end;beg++){
cout<<*beg<<" ";
}
cout<<endl;
}
测试样例:
void test_mystl()
{
const int N = 5;
vector<int> a = {
3,5,4,1,2};
vector<int> b(5);
// 取反
transCalc(a,b,N,InvT<int>);
outputCont("Inv a:", b.begin(), b.end());
// 取平方
transCalc(a,b,N,SqrT<int>);
outputCont("Sqr a:", b.begin(), b.end());
// 二值化
transCalc(a,b,N,MyThreshold<int>(2));
outputCont("Sqr a:", b.begin(), b.end());
// sort函数使用自定义排序方法
sort(a.begin(), a.end(), MyCompare<int>);
outputCont("Sort a by max:", a.begin(), a.end());
// sort函数使用自定义排序类
sort(a.begin(), a.end(), MyComp<int>(-1));
outputCont("Sort a by min:", a.begin(), a.end());
}
测试结果:
本篇博客的图像处理依赖于C++opencv开源算法包
首先定义一个操作模板函数:
// 对于图像而言的运算(使用自定义运算函数 MyOperator)
template <class MyOperator>
void transCalc(Mat &src, int w, int h, MyOperator op)
{
for(int row=0;row<h;row++){
for(int col=0;col<w;col++){
// 图像操作
src.at<uchar>(row, col) = op(src.at<uchar>(row, col));
}
}
}
// 类操作模板,灰度对数变换:
// 由于二值化除了传入变量本身还需要传入阈值,因此使用类来定义
template<class T>
class logTransform
{
public:
int C;
double Gamma;
// n默认是128
logTransform(int c=1, double gamma = 1.0):C(c),Gamma(gamma){
}
// 重载操作符"()",一旦使用()传入参数就执行自定义内容:
int operator()(T val)
{
float Val = float(val)/255;
return 255*C*log(1+Val*(Gamma-1)) / log(Gamma);
}
};
测试样例:
int main(int argc, char *argv[])
{
// 打开灰度图像
Mat img = cv::imread("C:\\Users\\S.E\\Desktop\\c++\\opencv_qt\\rice.png", 0);
int w = img.cols;
int h = img.rows;
imshow("original", img);
transCalc(img, w, h ,logTransform<uchar>(1, 0.01));
imshow("gamma=0.01", img);
waitKey(0);
return 0;
}
测试结果:
// 类操作模板,二值化:
// 由于二值化除了传入变量本身还需要传入阈值,因此使用类来定义
template<class T>
class MyThreshold
{
public:
int threshold;
// n默认是128
MyThreshold(int n=128):threshold(n){
}
// 重载操作符"()",一旦使用()传入参数就执行自定义内容:
int operator()(T val)
{
return (val > threshold)*255;
}
};
测试样例:
int main(int argc, char *argv[])
{
// 打开灰度图像
Mat img = cv::imread("C:\\Users\\S.E\\Desktop\\c++\\opencv_qt\\rice.png", 0);
int w = img.cols;
int h = img.rows;
imshow("original", img);
transCalc(img, w, h ,MyThreshold<uchar>(128));
imshow("Threshold=128", img);
waitKey(0);
return 0;
}
测试结果:
set
是c++stl标准库实现的一个容器,能够给予数据进行自动排序。其基本的数据结构基于红黑树,因此其在插入和删除的效率上会比一般的序列容器高,比如vector
。
接下来我们将以set为基础实现一个非常超级无敌简易的学生管理“系统”,方便大家更好的理解。
首先定义一条学生信息的基本组成,封装成一个类:
// 学生类
class studentInfo
{
public:
int _strNo; // 学号
string _strName; // 姓名
// 构造函数
studentInfo(int strNo, string strName){
_strNo = strNo;
_strName =strName;
}
// 重载 << 输出
friend ostream& operator<<(ostream& os, const studentInfo& info)
{
os<<endl<<info._strNo<<" "<<info._strName;
return os;
}
// 重载比较运算符(只比较学号)
friend bool operator<(const studentInfo& info1, const studentInfo& info2)
{
return info1._strNo<info2._strNo;
}
};
接着我们定义一个管理学生信息的类,用集合set存储每条学生信息,并且定义一些管理学生信息的基本增删改查方法:
值得注意的是,在对容器使用for循环遍历时可以使用auto
自动声明一个迭代器。
template <class T>
class students
{
public:
//存储学生信息
set<studentInfo> stuSet;
int stuNum = 0;
// 构造函数
template <class t>
students(t stud){
for(auto it:stud){
stuSet.insert(it);
stuNum ++;
}
}
// 增加一个元素
void add_single(T stu){
stuSet.insert(stu);
stuNum ++;
}
// 批量增加元素
template <class t>
bool add_batch(t stu){
for(auto it:stu){
stuSet.insert(it);
stuNum ++;
}
return true;
}
// 根据学号删除元素
bool del(int No){
for(auto it:stuSet){
if(it._strNo == No){
stuSet.erase(it);
stuNum --;
return true;
}
}
return true;
}
// 根据姓名删除元素
bool del(string Name){
for(auto it:stuSet){
if(!Name.compare(it._strName)){
stuSet.erase(it);
stuNum --;
return true;
}
}
return true;
}
// 根据学号查找姓名
string searchName(int No){
for(auto it:stuSet){
if(it._strNo == No){
return it._strName;
}
}
return "Not Found";
}
// 根据姓名查找学号
int searchNo(string Name){
for(auto it:stuSet){
if(!Name.compare(it._strName)){
return it._strNo;
}
}
return -1;
}
// 根据学号修改姓名
void update_no(int No, string afterName){
for(auto it:stuSet){
if(it._strNo==No){
stuSet.erase(it);
break;
}
}
stuSet.insert(studentInfo(No, afterName));
}
};
值得注意的是,set涉及查找并删除的实现方法中,若我们删除完set中的一条数据,应该直接return或者break退出查找的循环。这是因为在删除一条数据之后,set中的树形结构发生了改变,由于set内部的树遍历机制。导致erase 之后不能从那个起点再往回遍历,因此如果下一条数据正好在当前数据的父节点之上,往回遍历就会出现野指针的错误。即:
... ...
for(auto it:stuSet){
if(it._strNo==No){
stuSet.erase(it);
break;
}
}
... ...
测试样例:
void testStuSet()
{
vector<studentInfo> stu1;
stu1.push_back(studentInfo(11, "haotianY"));
stu1.push_back(studentInfo(13, "jinyuG"));
stu1.push_back(studentInfo(47, "jiawenL"));
vector<studentInfo> stu2;
stu2.push_back(studentInfo(41, "zhaohongH"));
stu2.push_back(studentInfo(19, "chenxiS"));
stu2.push_back(studentInfo(22, "yihaoW"));
stu2.push_back(studentInfo(49, "haohaoW"));
students<studentInfo> stuSet(stu1);
// 增
stuSet.add_batch(stu2);
stuSet.add_single(studentInfo(22, "xiangdongX"));
// 删
stuSet.del(22);
// 改
stuSet.update_no(47, "xiuwenL"); // 根据学号修改姓名
// 查
cout<<stuSet.searchName(49)<<endl; // 根据学号查找姓名
cout<<stuSet.searchNo("haohaoW")<<endl; // 根据姓名查找学号
outputCont("student Set:\n", stuSet.stuSet.begin(), stuSet.stuSet.end());
}
测试结果:
map 是一个关联容器,它提供一对一的数据处理能力(其中第一个(first)称为键,第二个(second)称为值。不同键的值可以相同),由于这个特性,它能在我们处理一对一数据的时候,在编程上提供快速通道。同时,和set一样,map内部也是自动排序的。
map应用之:输入一个字符串,用map统计每个字符出现的次数并输出字符及对应的次数。
//输入一个字符串,用map统计每个字符出现的次数并输出字符及对应的次数
void countStr(string str){
map<char, int> count;
for(int i = 0;i<str.length();i++){
// 如果第一次出现就赋值为1
if(count.find(str[i])==count.end()){
count[str[i]] = 1;
}
// 否则++
else{
count[str[i]]++;}
}
// 打印每个单词出现的次数
for(auto i:count){
cout<<i.first<<": "<<i.second<<" ";
}
}
测试样例:
int main()
{
countStr("iuewfhjsdbfkrhedjskjloisgjipjdzzsvdfgkzlkjbhgvdvjj");
return 0;
}
测试结果:
本次实验基于前一次实验介绍的模板templet以及C++标准库中的STL库,实现了自定义的算法及函数并进行了简易的数字图像处理。同时,通过学生信息管理以及统计字符串中每个字母出现的次数这两个例子初步了解了set和map的基本使用方法,至此,c++对我而言算是正式敞开了遮掩的大门,露出了些许微不足道的光亮。
更多的基于C++的应用将作为我本学期的课设呈现。