STL和泛型编程
Week6 Notes
1.模板概念和模板函数
C++模板简介
概观
为什么会有模板这个概念
STL里面可能会有大量的重载出现
模板是C++的一种特性,允许函数或类对象通过泛型的形式表现或运行
模板可以使得函数类或类在对应不同的type是能正常工作,不需要为每一个性别都写一次
C++主要有两种类型的模板,一种是类模板,一种是函数模板
模板的实例化
声明并未给出一个函数或类的完全定义,只是提供了一个函数或类的语法框架syntactical skeleton
实例化是指从模板构建出一个真正的函数或类的过程
实例化有两种类型,一种是显式实例化,一种是隐式实例化
定义函数模板,可以使用class代替typename,但从语义上来讲更好,防止以为只有类可以,实际上任何型别都可以所以尽量使用typename
函数模板的使用
不可以使用不同型别的参数来调用,因为编译器已经知道函数需要传递的型别
模板实例化,用具体的型别代替模板参数T的过程叫做实例化,生成了一个模板实例。一旦使用了函数模板,这种实例化的过程会自动触发,不需要额外去请求实例化
结论是函数模板被编译了两次,第一次是检查模板代码本身是否有语法错误,第二次是在实例化期间检查对模板代码的调用是否合法。
参数的推导不允许自动型别的转换,每个T必须严格匹配,当不匹配时,有两种方法来强制转换参数的类型,static_cast或者显式指定T的型别
函数模板的重载,非模板函数可以和同名的模板函数共存,编译器通过参数推导来决定用哪个
当其他因素都相同的时候调用非模板函数,,,,允许空模板函数,,,,显示指定的时候无需参数推导。对于型别不同的参数只能调用非模板函数。
所有重载版本的声明必须位于他们被调用的位置之前。
2.类模板和操作符重载
与函数模板类似,类也可以通过参数泛化,从而构建出一族不同型别的类实例。
类模板实参可以是某一型别或者常量(仅限int或者enum)
关键词typename和class都可以用,但倾向于typename。
在函数内部,T可以像其他型别一样来定义成员变量和成员函数
当我们去实例化class的时候,
要定义一个类模板的成员函数,则要指明其实一个模板函数,写在类里面的话就不需要
当两个尖括号连续用到的时候要分开。
类模板的特化
特化一个类模板意味着需要特化其他所有参数的成员函数
方法是写一个空的模板参数,然后在下面显示调用
类模板的偏特化,
上面声明一样,但在类的后面跟上限制条件,偏特化是还含有模板参数,全特化是指定了型别。如果有不止一个偏特化同等程度地能够匹配某个调用,那么调用会具有二义性,编译器不会通过。不能使用具有二义性的应用。
默认模板实参:
template >
class Stack{
private:TContainer m_Container;
};
C++操作符重载
Operator定义了一种特殊的函数,该函数的行为是将操作符应用于某一特定的型别,使之能通过该操作符进行操作
操作符重载给出了操作符的不同含义
编译器通过具体型别来识别某个操作符在该型别上的意义
本质上就是函数,
大多数内置的操作符支持重载
一些规则
不能定义一种新的操作符
内置型别不能重载
操作符重载的两种情况:
一种是非静态的成员函数,一种是静态的全局函数,如果该全局函数要访问类的private或者protected,要声明成friend成员
一元操作符如果是成员函数,,,则没有参数,如果全局函数则有一个参数
二元如果是成员函数则有一个参数,全局函数择优两个参数
操作符重载不能带有默认值,除了operator=,其他操作符重载可以被子类继承。
3.泛型编程
泛型编程是一种编程方法,以一种to-be-specified-later的方式给出,等到需要调用的时候,再以参数方式,实例化一个具体的方法或对象。
大多数面向对象语言都支持泛型编程。
C++里的泛型通过模板来体现
关联特性(traits)
我们有时候定义了很复杂的类型,但因为类型的问题得到了错误的结果,这时候我们要用特性Traits,为每个Sigma函数的参数型别T分别创建一种关联,关联的型别就是用来存储Sigma结果的型别。
这种关联可以看做是型别T的一种特性,因此Sigma函数返回值的类型叫做T的Trait,
T->association->characteristic ofT->another type->trait
Traits可以实现为模板类,而关联则是针对每个具体型别T的特化
Traits的实现
Template classSigmaTraits {};
Template <> classSigmaTraits{
Public: typedef int returnType;
};
template <> classSigmaTraits{
public: typedef int returnType;
};
现在sigma函数可以写成
template
inline typenameSigmaTraits::ReturnType sigma((const T const* start, const T const *end){
typedef typename sigmaTraits::ReturnTypeReturnType;
returnType s = ReturnType();
while(…)
};
迭代器
迭代器是指针的泛化,本身是一个对象,指向另外一个可以被迭代的对象。
用来迭代一组对象,即如果迭代器指向一组对象中的某个元素,则通过increment以后它就可以指向这组对象中的下一个元素。
在STL中迭代器是容器和算法之间的借口
算法通常以迭代器作为输入参数
容器只要提供一种方式,可以让迭代器访问容器中的元素即可。
在STL中迭代器最重要的思想就是分离算法和容器,迭代器将算法和容器粘合在一起从而使得一种算法的实现可以运用到多种不同的容器上。
Template
Inline_init find(_init _First, _init _Last,const _Ty& _Val){
//find first matching _Val
for(; _First != _Last, ++ _First)
if(*_First== _Val)
break;
return (_First);
};
find算法对于不同的容器均使用
每种容器都有其对应的迭代器。
Std::vector v(…);
Std::vector::iterator itv =std::find(v.begin(), v.end(), elementToFind)
4.容器
vector是一个能够存放任意型别的动态数组,vector的数据结构和操作与数组类似在内存中的表现形式是一段地址连续的空间。
Vector与数组的区别在于数组大小定义就固定的,vector支持动态空间大小元素加入会自动扩充。
#include
int main(){
std::vector v;
}
std::vector v;
std::vector v(n);
std::vector v(n,i);
std::Vector copyOfV(v);
int array[] = (1,2,3);
std::vector v(array, array+3);
vector中加入元素
std::vector v3;
for(std::size_t I = 0; i<10; i++){
std::wstringstream wss;
wss << TEXT(“String[”] << I <
v3.push_back(wss.str());
}
判断vector是否为空,获取vector大小
如果要判断vector是否为空,则调用empty()函数
如果要获取vector大小则调用size()
访问vector中的元素有两种方法,vector::at()做边界检查,效率不如operator[]
try{
std::wstringwsz1 = v[5]; //not bounds checked –will not throw
std::wstringwsz2 = v.at[5]; //bounds checked – will throw
}
catch(conststd::exception& ex){
console::WriteLine(ex.what());
}
删除vector中的元素,
clear清楚整个vector
pop_back弹出vector尾部元素
erase删除vector中某一位置的元素
删除方法一
std::vector::const_iterator it = v.begin();
v.erase(it+1);//erasethe second element in the vector
删除方法二
通过某一条件函数找到vector中需要删除的元素,所谓条件函数是一个按照用户定义的条件返回true、false的函数对象
remove_if函数定义在algorithm中,需要include
在erase函数中调用remove_if执行删除
v.erase(std::remove_if(v.begin(), v.end(), ContainsString(L”C++”)),v.edn());
struct ContainString: publicstd::unary_function{
ContainsString(const std::wstring& wszMatch):m_wszMatch(wszMatch) {}
Bool operator() (const std::wstring& wszString ToMatch)const{
Return(wszStringToMatch.find(m_wszMatch)
!=-1)
}
std::wstring m_wszMatch;
};
remove_if是不是真正溢出了vector中的元素呢?
Remove_if做的事针对ContainsString条件给出了erase函数需要操作的iterator位置
找出所有需要删除的元素移到最后,要保留的顶上去
Deque是一个双向队列,提供的函数与vector类似,新增了两个函数push_front和pop_front,deque采用了与vector不同内存管理方法,大块分配内存,为了使用deque必须用include指令包含如下文件,并通过std命名空间去访问。
#include
std::deque dq;
List是一个能存放任意型别的双向列表double linked list
可以向list中接入一个子链表sub-list,空间并不需要是连续的,是链式结构
对于插入,删除和替换等需要重排序列的操作,效率极高
对于移动元素到另一个list,把一个排好序的list合并到另一个list操作,实际上只改变list接点间的链接,并没有发生元素复制。
List的劣势是只能以连续方式存取list中的元素,查找任意元素的平均时间和list的长度成线性比例,对于查找,随机存取等元素定位操作效率低。
在每个元素接点上增加一些较为严重的开销,即每个接点的前向和后向指针。
Std::list l;
Std::list l(n);
删除list中的元素
//remove string that constains “C++”
l.remove_if(ContainsString(TEXT(“C++”)));
用法一
std::list::const_iteratro it =l.begin();
l.erase(it);
l.erase(std::remove_if(l.begin(), l.end(), ContainsString(L”C++”)),l.end());
向list中某一位置插入元素
粘结list
splice实现了list的粘结功能,将一个list的部分元素或全部元素查到另一个list
std::list::const_iteratro it1 =list1.begin();
list1.splice(it1, list2);
it1++;
list2.splice(list2.begin(), list1, it1);
it1 = list1.begin();
std::advance(it1, 3);
list1.splice(list1.begin(), list1, it1, list1.end());
在本次作业中,我一开始以为需要自己重构一个Not_equal_to函数模板来使用,后来发现STL中已经有内置的该函数,而且是一种仿函数的方式,由于仿函数的内容在下一节中,我自己查询了仿函数的一些概念,仿函数是一种类内定义operator()的方式来达到和函数同样的使用目的,好处是对于非inline函数,仿函数在编译期间就可以展开,增加效率,而缺点是不能处理有异质类型的函数。仿函数的使用分类有生成器,一元函数,二元函数。而返回值为Bool的可以分类为一元谓词,二元谓词等。
在调用仿函数的过程中,我一开始不理解如果和调用,在错误信息的提示下以及查找,知道了这与我之前学习的硬件语言有异曲同工之妙,相当于把某些简单的算法分装成模块(函数模板),然后可以被其他的模块调用,这样做的最大的好处是将这些简单的原酸结果可以当做其他模块的输入,特别是返回值boolean值的一些判断函数。
在查找很多的资料后我觉得下列的一些用例写法我是比较能够接受和理解的
Arithmetic binary functors
plus;
f(arg1, arg2) returns the value arg1 + arg2.
minus;
f(arg1, arg2) returns the value arg1 - arg2.
multiplies;
f(arg1, arg2) returns the value arg1 * arg2.
divides;
f(arg1, arg2) returns the value arg1 / arg2.
modulusf;
f(arg1, arg2) returns the value arg1 % arg2.
这些仿函数的源代码一般都是继承了binary或者unary函数,这两个函数的作用是讲输入输出的类型统一,我觉得相当于之前讲的traits,然后类内有operator(),来达到可以和函数一样使用的目的。