C和C++重难点复习笔记(三)【泛型编程与STL】

最近抓紧最后两天空闲时间,把《C和C++程序员面试秘笈》中的第10章【泛型编程】和第11章【STL(标准模板库)】的内容学习了一下。这两章的内容说实话觉得有点少,可能不太够用。因此我补充了很多额外的内容。随着学习的进一步深入,我会一直往里补充,希望大家关注。

之前的内容总结在:C和C++重难点复习笔记(一)【面向过程】

C和C++重难点复习笔记(二)【面向对象】

欢迎大家参考。


第十章 泛型编程

1、使用泛型编程(模板)可以极大增强代码重用性。

template<class T>  //这class不代表对象的类而是类型,可以用typename替换;参数数目还可以继续增加
T max(T a,T b)
{
    return a>b? a:b;
}

使用的时候,传入的数据如果是float的,则为max(1.1f, 2.2f),如果是double则f改成L的小写,或者直接指定int类型,用法为max<int>(1,2.0)

2、函数模板是一个模板,模板函数是一个由函数模板生成的函数。类模板同理,是一个模板,而模板类是指用类模板生成的类。

值得注意是类模板的实例化必须显示的指定。比如已经定义了一个类模板Point,定义实例的时候必须是Point<int>显示的指定。

3、类模板的成员函数只有在使用的时候才会被实例化。但是使用模板的缺点:会导致代码的膨胀,影响程序的运行效率。解决方式是把与参数无关的代码分离出来。

4、模板类的实例个数是由类型参数的种类决定的。

5、模板的特化:有两种,函数模板的特化和类模板的特化。

也就是对模板的的一种特殊化处理,在调用该函数或类时,如果传入的参数满足这种特殊化情况,则调用自己定义的特化类或者函数,而不使用原先的模板。(比如判断两个字符串是否相等,就和判断两个整形数据不同,需要用strcmp函数)

方式:

template<> //里面为空
之后……照常写你的类或者函数,记得与之前类模板或者函数模板的中同名

6、部分模板特例化和全部模板特例化:

两种全是特例化,部分模板特例化是指模板参数没有被全部指定,而全部模板特例化是指模板参数全部被指定为特定的类型。

7、函数模板和类模板使用的时候没什么难点,就是得注意类模板实例化的时候,一定要在类名的后面显示的加上参数<int....>等等之类的才能再定义对象。


第十一章 STL(标准模板库)

1、STL有容器算法迭代器组件。

2、三种容器:

序列容器:vector、string、deque、list。序列容器中每个元素在内存中都有固定的位置,除非用插入或者删除操作。

关联容器:set、multiset、map和mutimap。关联式容器是非线性的树结构。

适配容器:stack、queue和priority_queue。适配容器是让一种已存在的容器类型采用另一种不同的抽象类型的工作方式来实现的一种机制。

标准容器类

特点

顺序性容器

vector

从后面快速的插入与删除,直接访问任何元素

deque

从前面或后面快速的插入与删除,直接访问任何元素

list

双链表,从任何地方快速插入与删除

关联容器

set

快速查找,不允许重复值

multiset

快速查找,允许重复值

map

一对多映射,基于关键字快速查找,不允许重复值

multimap

一对多映射,基于关键字快速查找,允许重复值

容器适配器

stack

后进先出

queue

先进先出

priority_queue

最高优先级元素总是第一个出列

 

表格摘自小喾的博客-STL 顺序容器,关联容器

3、STL如何实现vector?

template<class _Ty, class _A=allocator<_Ty> >
class vector
{
......
}

 模板参数有两个,第一个是需要自己定义的,即vector中的元素类型;第二个是默认的,allocator<_Ty>类型,这是一种内存配置器,负责提供内存管理。初始化vector没有元素的时候,容量为0。之后填入第一个数据,分配一个。然后再填,不够用的时候每次会重新分配两倍的内存,然后拷贝原数组的内容

vetcor使用的时候要注意:

1) 定义时需要指定类型,例如 vector<int> v;

2) 插入元素用push_back(),再最后插入

3) 弹出用pop_back(),删除最后一个元素

4)遍历有几种方式:

vector<int> v;
//....pushback数据
for(vector<int>::iterator itor=v.begin();itor!=v.end();itor++)
	cout<<*itor<<" "; 
for(v::iterator itor=v.begin();itor!=v.end();itor++) //这样不行
	cout<<*itor<<" "; 
for(vector<int>::size_type j=v.size(); j>0;j--)//这样可以
    cout<<v[j-1];
for(vector<int>::size_type j=v.size()-1; j>=0;j--)//这样不行
    cout<<v[j];

第一v.end()不存数据,第二size_type是无符号整数,永远大于0,因此j>=0永远不会为0。

5) v.capacity()表示数组目前有多大的容量,而v.size()表示数组中目前有多少个元素。

6) 调用v.erase(itor)可以删除数组中由迭代器itor指向的元素,但是删除以后,被删除的元素会自动前移。因此如果是在循环中,应该手动控制itor减1,以免漏项

(注意,序列容器vector、string、deque和list的erase是有返回值的,返回下一个元素的迭代器。因此可以it=v.erase(it)。而关联容器set、multiset、map和multimap的erase没有返回值。比如map<int,int> m,之后在循环中it为迭代器,可以m.erase(it++),这个叫“后置递增迭代器”)

7) 可以调用algorithm中的remove算法删除vector中的元素,但是其实并不会真正删除,而是把想要remove的元素放到了数组最后面,返回一个指向新的vector的结尾的iterator,从开始到新这个新的结尾(不含新结尾元素)的范围包含了remove操作后剩余的所有元素。

8) v.reserve()可以用来手动设置容器的容量。 v.resize()也可以,但是reserve是相当于重新分配,需要n大于目前的大小 。而后者resizen可以随便设置,如果小于目前的n,则多出来的元素会被销毁。

9) v.insert()函数可以在v中插入元素,具体使用方法有三种:(deque也是相同用法)

//迭代器loc处插入val
insert(iterator loc,const type &val)

//迭代器loc处插入num个val
insert(iterator loc,size_type num,const type &val)

//迭代器loc处插入迭代器[start,end)处的内容,左闭右开,erase也是一样
insert(iterator loc,input_iterator start,input_iterator end)

 

4、list和vector的区别:

1)vector是连续内存空间,list是双向链表实现的因此内存可以是不连续的。

2)list可以push_back,push_front,pop_back,pop_front,而vector只能push_back和pop_back。

3)vector,deque,map,string这些都可以用[ ]访问,list不行

4)list的迭代器只能自增自减,不允许+,+=,<(比较)等操作。

5)vector中的元素被v.erase()以后,里面的元素会自动前移。而list中的元素被l.erase()以后,会释放链表的节点内存,会提供一个返回值,指向原先的下一个节点。所以如果是在循环中,应当令itor=l.erase(),然后再控制itor减1。(两者都会有一个返回值,对象为该位置的迭代器)

因此如果需要高效随机存取就选vector,如果需要高效插入与删除就用list。

5、deque是一种动态数组,表示双向队列。其中比vector多了push_front和pop_front。另外,虽然地址也是连续的,但是没有capacity和reserve函数,因为deque用的是一段一段的定量内存,内存扩充的时候只加一段定量内存。不存在容量的概念。

6、适配器stack和queue:

queue作为队,stack作为栈,本身是有其自己的特性的。都有push和pop两个函数,但是pop一个操作的是队头,一个是栈顶。

适配器可以用别的数据结构作为基本存储单元,例如:

stack<int,vector<int> > s;//注意两个>>之间应该空一格
queue<int,vector<int> > q;

这样基本单元成了vector。

但是,由于vector不能popfront,因此上面定义的queue其实不能pop出队头元素。此外,访问队头和队尾的元素,用front和back来访问。

而stack的push和pop都是对栈尾元素进行操作,和vector一样,因此上面的定义是可以的。此外,pop只是弹出元素,如果想用栈顶的元素,要用top才能返回。

7、关联容器set集合可以插入、删除元素,插入用s.insert(xxx),删除用s.erase(xxx),但是实际不会插入重复的元素。multiset可以存放重复的元素。此外主要用途是,multiset<int,greater<int> > minset为例,minset.begin()是最大的数。默认应该是less的,也就是说begin是最小的数。

8、关联容器map:其中存放的每一个元素都是一个键值对。

1)两种插入方式:

map<int,string> mapstring;

mapstring.insert(pair<int,string>(1,"one"));
mapstring[3]="three";

2)pair<int,xx>中第一个必须得是int。

3)用insert插入键-值对,如果键已经有了,则插入会失败。但是用[ ]直接指定,如果没有就是插入,如果有就是修改。

4)可以用mapstring.find(2)函数寻找键为2的元素,并且返回一个迭代器对象。如果没找到,则返回最后的end()位置。

5)map的迭代器itor,(*itor).first表示键,(*itor).second表示值。

6)对map进行遍历,默认是按键(key)的大小进行升序排列。也可以指定为降序,

方法为:把map和iterator的声明都改为<int,.xxx(比如string),greater<int> >(不过要注意,第一,只是map和iterator的声明改,插入时的pair<int,string>不改。第二,greater<int> >的后两个">"中间要有一个空格,与输出的>>所区分。这样就变成了从大到小排序而不是默认的从小到大排序。

7)map和hash_map的用法一样,表面上看,外部提供的函数和数据类型也一致。但是内部,map是用红黑树实现的,hash_map是通过哈希表实现的。

补充:对于vector建大根堆和小根堆,可以是push_heap(myvector.begin(),myvector.end(),less<int>()),意思是对刚插入的尾部元素进行堆排序。这里的less<int>()多了个括号,同样也是从小到大,但是因为vector的push_back在后面,因此意思是从后面往前面看从小到大,即vector中第一个数[0]是最大的。如果是push_heap(myvector.begin(),myvector.end(),greater<int>()),则是从后往前是从大到小,即vector中第一个数是最小的。同样还有pop_heap(myvector.begin(),myvector.end(),less<int>()),意思是把[0]号元素移到末尾(即把最大的移动到末尾,准备弹出,然后剩下的重新堆排序)

 

9、标准的STL关联容器内部是一个平衡二叉树。STL的底层机制都是红黑树完成的。红黑树要求所有节点为红色或者黑色,但是根节点必须是黑色,叶节点是空节点被着色为黑色。如果父节点是红色,两个子节点应该是黑的。节点到子孙节点的每条简单路径上都包含着相同数目的黑色节点。map的底层就是用红黑树实现的。

10、map和hash map的区别在于map底层是红黑树,hashmap底层是哈希表;map是标准的一部分,hashmap不是;map的优点在于元素可以自动按照键值排序,hashmap的优点在于各项操作平均复杂度接近于常数。

11、STL算法可以巧妙地处理存储在容器中的数据。reverse算法可以逆置一个区间中的元素。用法:

#include<algorithm>
int a[4]={1,2,3,4};
reverse<int [4]>(a,a+4);
reverse< vector<string>::iterator >(v.begin(),v.end());

用完reverse以后,容器或者数组中的元素就已经逆置了,再次遍历输出是逆置以后的。

12、find泛型算法:pos=find(coll.begin(),coll.end(),2);从coll的begin到end寻找值为2的位置。

for_each泛型算法:for_each(pos1,pos2,print); 从[pos1,pos2)中的每个元素调用print函数。左闭右开。

反向迭代器reverse_iterator使用:

deque<int>::reverse_iterator rpos1(pos1);
deque<int>::reverse_iterator rpos2(pos2);
for_each(rpos2,rpos1,print)

其实就是把当前的迭代器位置减一,然后之后调用的时候反一下。比如从[1,7),1变成0,7变成6,然后变成了[6,0)倒着输出。

13、智能指针:这块抄袭这里

  • auto_ptr:可以自动释放指向的内存。

用法,例如有个test类:

auto_ptr<test> Testptr(new test()); 这样可以。

但是用=号不行,即auto_ptr<test> Testptr=new test();是不行的。

auto_ptr不能以传值的方式进行传递,并且一个对象只能有一个拥有者,严禁一物二主。(所以它对别的指针传值,相当于是“传递”所有权,之后它就失去了对原先的变量的控制权。)

auto_ptr是C++98的方案。

  • share_ptr:

shared_ptr允许多个指针指向同一个对象,即一个指针对另一个传值,之后它仍然拥有对原先变量的控制权。内部还会有个引用计数,当最后一个指针失效的时候,才会释放内存。

  • unique_ptr:

这个和auto_ptr很像,都是一个对象一个所有者。但C++11中已经放弃auto_ptr转而推荐使用unique_ptr。

auto_ptr通过拷贝构造或者operator=赋值后,对象所有权转移到新的auto_ptr中去了,原来的auto_ptr对象就不再有效,这点不符合人的直觉。unique_ptr则直接禁止了拷贝构造与赋值操作。(但是可以用move来转移所有权,此外,还可以当函数返回值用来实现所有权的转移。)

此外,auto_ptr不可做为容器元素,会导致编译错误。虽然unique_ptr同样不能直接做为容器元素,但可以通过move语意实现。

auto_ptr<int> ap(new int(10));
auto_ptr<int> one (ap) ; // ok
auto_ptr<int> two = one; //ok

unique_ptr<int> ap(new int(10));
unique_ptr<int> one (ap) ; // 会出错
unique_ptr<int> two = one; //会出错

unique_ptr<int> uPtr2 = std:move( up) ; //这里是显式的所有权转移. 把up所指的内存转给uPtr2了,而up不再拥有该内存

unique_ptr<int> sp(new int(88) );
vector<unique_ptr<int> > vec;
vec.push_back(std::move(sp));
vec.push_back( sp );//这个不对,会报 error:
  • weak_ptr:

weak_ptr是一种不控制所指向对象生存期的智能指针,它指向一个由shared_ptr管理的对象,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数。一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放,即使有weak_ptr指向对象,对象还是会被释放。 这个赋值后也是共享对象。

  • scoped_ptr:

scoped和weak_ptr的区别就是,给出了拷贝和赋值操作的声明并没有给出具体实现,并且将这两个操作定义成私有的,这样就保证scoped_ptr不能使用拷贝来构造新的对象也不能执行赋值操作,更加安全,但有了”++”“–”以及“*”“->”这些操作,比weak_ptr能实现更多功能。
 

14、auto:

std::string str = “hello, world”;  
for(auto ch : str) {  
     std::cout << ch << std::endl;  
}  

有点像python中的“ for i in A:”,可以在for循环中利用auto遍历字符串、数组、stl容器等。

15、函数对象就是一个重载了“()”的类的对象。用法:

class Myminus
{
public:
    int operator()(int a,int b){return a-b;}
};

int main()
{
    int a=1;
    int b=0;
    Myminus minusObj;
    cout<<minusObj(a,b);
    return 0;
}

此外,STL中提供了一元函数和二元函数两种函数对象。一元有negate,相反数;二元有plus、minus、multiplies、divides、modulus(求余)、equal_to、not_equal_to、greater(大于)、greater_equal(大于等于)、logical_and(逻辑与)

具体用的时候,记得加上模板参数。例如minus<int> int_minus;之后再int_minus(3,5)。

16、bind1st和bind2nd:

函数适配器,可以把上面说的二元函数对象转换为一元函数对象。

binder1st<plus<int> > plusObj=bind1st(plus<int>(),10);

这样plus(10,4)就可以用plusObj(4)来代替。

注意,第一,前面是binder1st,后面没有er。第二,前面模板参数传入的是plus<int>,后面函数形参传入的是plus<int>(),多一个括号。

如果绑定第二位,则换成binder2nd,后面换成bind2nd。

17、count_if函数:

int n=count_if(v.begin,v.end(),bind1st(less_equal<int>(),6));
//即为从begin到end之间,比6大的元素的个数。因为<=的6被绑定在左边

18.显示转换关键字:

  • static_cast< new_type >(expression) 
  • dynamic_cast< new_type >(expression) 

new_type为目标数据类型,expression为原始数据类型变量或者表达式。dynamic_cast具有类型检查的功能,比static_cast更安全。

这个需要补充:

上行转换:子类的指针或者引用转换成基类表示(也就是基类指针指向子类的对象)

下行转换:基类指针转换成子类表示(子类指针指向基类对象)。

常规来说,是只能上行转换。但是下行转换并不是完全不可以。要进行下行转换,需要用到两个关键字:dynamic_cast和static_cast。

对于上行转换,static_cast和dynamic_cast都安全,对于下行转换,static_cast不会进行类型检查。而dynamic_cast会进行类型检查,如果不能转换(子类指针想指向基类对象,这是不行的,但是不会报错)会直接返回null。此外,用dynamic_cast的前提是需要基类中必须存在虚函数(需要满足多态类型,以及从上到下的继承关系)。

  • const_cast <type_id> (expression)

该运算符用来修改类型的const或volatile属性。除了const 或volatile修饰之外, type_id和expression的类型是一样的。
常量指针被转化成非常量指针,并且仍然指向原来的对象;常量引用被转换成非常量引用,并且仍然指向原来的对象;还有常量对象被转换成非常量对象。

  • reinterpret_cast <type-id> (expression)

type-id必须是一个指针、引用、算术类型、函数指针或者成员指针。
它可以把一个指针转换成一个整数,也可以把一个整数转换成一个指针(先把一个指针转换成一个整数,
再把该整数转换成原类型的指针,还可以得到原先的指针值)

这块需要补充一个示例:

char str[]="glad to test something";
char *p=str;
p++;
int *p1=reinterpret_cast<int *>(p);
p1++;
p=reinterpret_cast<char *>(p1);
printf("result is %s\n",p);

p++后,p指向第二个元素l的位置。

p1的类型为int型,p1++后p1指向的位置就增加了4个字节,因此最后输出的内容是“to test something”。

19、string用法总结: C++中的String的常用函数用法总结

20.lambda表达式:

  • [] // 沒有定义任何变量。使用未定义变量会引发错误
  • [x, &y] // x以传值方式传入(默认),y以引用方式传入
  • [&] // 任何被使用到的外部变量都隐式地以引用方式加以引用
  • [=] // 任何被使用到的外部变量都隐式地以传值方式加以引用
  • [&, x] // x显式地以传值方式加以引用。其余变量以引用方式加以引用
  • [=, &z] // z显式地以引用方式加以引用。其余变量以传值方式加以引用
vector<int> iv{5, 4, 3, 2, 1};
int a = 2, b = 1;

for_each(iv.begin(), iv.end(), [b](int &x){cout<<(x + b)<<endl;}); // (1)

for_each(iv.begin(), iv.end(), [=](int &x){x *= (a + b);});     // (2)

lambda表达式,能够用于创建并定义匿名的函数对象,以简化编程工作。

[]内的參数指的是Lambda表达式能够取得的全局变量。(1)函数中的b就是指函数能够得到在Lambda表达式外的全局变量,假设在[]中传入=的话,即是能够取得全部的外部变量。

21.volatile关键字:

定义为volatile的变量是指。该变量可能会被意想不到的改变。因此编译器在优化时,每次都从RAM读取这个变量的值,而不是使用保存在寄存器中的变量。

此外,一个参数可以既是volatile也是vonst 的。因为const的告诉程序不应试图去修改这个参数,而volatile是告诉程序,别从寄存器中读取这个数据,而是统统从内存中读取它。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值