【新特性】C++STL容器和C++11 新特性介绍(持续更新)

参考资料

C++STL容器和算法介绍

1. C++ 的 STL 介绍

STL ⼀共提供六⼤组件,包括容器,算法,迭代器,仿函数,配接器和配置器,彼此可以组合套⽤。容器通过配置器取得数据存储空间,算法通过迭代器存取容器内容,仿函数可以协助算法完成不同的策略变化,配接器可以应⽤于容器、 仿函数和迭代器。

  • 容器: 各种数据结构,如 vector, list, deque, set, map,⽤来存放数据, 从实现的⻆度来讲是⼀种类模板。

  • 算法: 各种常⽤的算法,如 sort(插⼊,快排,堆排序), search(⼆分查找), 从实现的⻆度来讲是⼀种⽅法模板。

  • 迭代器: 从实现的⻆度来看,迭代器是⼀种将 operator*,operator->,operator++, operator–等指针相关操作赋予重载的类模板,所有的 STL 容器都有⾃⼰的迭代器。

  • 仿函数: 从实现的⻆度看,仿函数是⼀种重载了 operator()的类或者类模板。 可以帮助算法实现不同的策略。

  • 配接器: ⼀种⽤来修饰容器或者仿函数或迭代器接⼝的东⻄。

  • 配置器: 负责空间配置与管理,从实现的⻆度讲,配置器是⼀个实现了动态空间配置、空间管理,空间释放的类模板。

2. STL 中序列式容器的实现

1. vector

vector概念
  • vector是动态空间,随着元素的加⼊,它的内部机制会⾃⾏扩充空间以容纳新元素。 vector 维护的是⼀个连续的线性空间,⽽且普通指针就可以满⾜要求作为 vector 的迭代器(RandomAccessIterator)。

  • vector 的数据结构中其实就是三个迭 代器构成的,⼀个指向⽬前使⽤空间头的 iterator,⼀个指向⽬前使⽤空间尾的iterator,⼀个指向⽬前可⽤空间尾的 iterator。如果容量不够,则容量扩充⾄两倍。

  • vector的实现方式是基于倍增思想的:假如vector的实际长度为n,m为vector当前的最大长度,当有新的元素插⼊时,如果⽬前容量够⽤则直接插⼊;如果当前的n=m-1,则下一次存储时动态申请一个2m大小的内存,将原有的数据拷⻉到新空间中,再释放原有空间,完成⼀次扩充。需要注意的是,每次扩充是重新开辟的空间,所以扩充后,原有的迭代器将会失效。反之,在删除的时候,如果n≥m/2,则再释放一半的内存。

vector容器的声明

vector 容器存放在模板库:#include<vector>里,使用前需要添加这个库。

#include<vector>
vector<int> vec;
vector<char> vec;
vector<pair<int,int> > vec;
vector<node> vec;
struct node{...};

vector容器的使用方法

vector容器是支持随机访问的,即可以像数组一样用[]来取值。

2. list

与 vector 相⽐, list 的好处就是每次插⼊或删除⼀个元素,就配置或释放⼀个空间,⽽且原有的迭代器也不会失效。 STL list 是⼀个双向链表,普通指针已经不能满⾜ list 迭代器的需求,因为 list 的存储空间是不连续的。 list 的迭代器必需具备前移和后退功能,所以 list 提供的是BidirectionalIterator。 list 的数据结构中只要⼀个指向 node 节点的指针就可以了。

3. deque

vector 是单向开⼝的连续线性空间, deque 则是⼀种双向开⼝的连续线性空间。所谓双向开⼝,就是说 deque ⽀持从头尾两端进⾏元素的插⼊和删除操作。相⽐于 vector 的扩充空间的⽅式, deque 实际上更加贴切的实现了动态空间的概念。 deque 没有容量的概念,因为它是动态地以分段连续空间组合⽽成,随时可以增加⼀段新的空间并连接起来

由于要维护这种整体连续的假象,并提供随机存取的接⼝(即也提供RandomAccessIterator),避开了“重新配置,复制,释放”的轮回,代价是复杂的迭代器结构。也就是说除⾮必要,我们应该尽可能 的使⽤ vector,⽽不是 deque。

deque模板存储在C++STL的#include<deque>中。

deque容器的使用方法

除了这些用法之外,deque比queue更优秀的一个性质是它支持随机访问,即可以像数组下标一样取出其中的一个元素。即:q[i]

4. stack

是⼀种先进后出的数据结构,只有⼀个出⼝, stack 允许从最顶端新增元素,移除最顶端元素,取得最顶端元素。 deque 是双向开⼝的数据结构,所以使⽤ deque 作为底部结构并封闭其头端开⼝,就形成了⼀个 stack。

stack容器的声明

stack容器存放在模板库:#include<stack>里,使用前需要先开这个库。

#include<stack>
stack<int> st;
stack<char> st;
stack<pair<int,int> > st;
stack<node> st;
struct node{...};
stack容器的使用方法

5. queue

是⼀种先进先出的数据结构,有两个出⼝,允许从最底端加⼊元素,取得最顶端元素,从最底端新增元素,从最顶端移除元素。 deque 是双向开⼝的数据结构,若以 deque 为底部结构并封闭其底端的出⼝,和头端的⼊⼝,就形成了⼀个 queue。(其实 list 也可以实现 deque)

queue容器的声明

queue容器存放在模板库:#include<queue>里,使用前需添加这个库。


#include<queue>

queue<int> q;

queue<char> q;

queue<pair<int,int> > q;

queue<node> q;

struct node{...};


queue容器的使用方法

就从使用方法来讲:

  • queue不支持随机访问,即不能像数组一样地任意取值。
  • queue不可以用clear()函数清空,清空queue必须一个一个弹出。
  • queue也并不支持遍历,无论是数组型遍历还是迭代器型遍历统统不支持,所以没有begin(),end();函数,使用的时候一定要清楚异同!

6. priority_queue

底层是⼀个 vector,内部实现是一个二叉堆。通过维护这个vector,以达到允许⽤户以任何次序将任何元素插⼊容器内,但取出时⼀定是从优先权最⾼(数值最⾼)的元素开始取的⽬的。

⼤根堆,是⼀个满⾜每个节点的键值都⼤于或等于其⼦节点键值的⼆叉树(具体实现是⼀个vector,⼀块连续空间,通过维护某种顺序来实现这个⼆叉树),新加⼊元素时,新加⼊的元素要放在最下⼀层为叶节点,即具体实现是填补在由左⾄右的第⼀个空格(即把新元素插⼊在底层vector 的 end()),然后执⾏⼀个所谓上溯的程序:将新节点拿来与⽗节点⽐较,如果其键值⽐⽗节点⼤,就⽗⼦对换位置,如此⼀直上溯,直到不需要对换或直到根节点为⽌。当取出⼀个元素时,最⼤值在根节点,取⾛根节点,要割舍最下层最右边的右节点,并将其值重新安插⾄最⼤堆,最末节点放⼊根节点后,进⾏⼀个下溯程序:将空间节点和其较⼤的节点对调,并持续下⽅,直到叶节点为⽌。

priority_queue容器的声明

priority_queue 容器存放在模板库:#include<queue>里,使用前需要先开这个库。

这里需要注意的是,优先队列的声明与一般STL模板的声明方式并不一样。

大根堆声明方式
大根堆就是把大的元素放在堆顶的堆。优先队列默认实现的就是大根堆。

#include<queue>
priority_queue<int> q;
priority_queue<string> q;
priority_queue<pair<int,int> > q;

另一种构建大顶堆的方法

priority_queue<int> q;
//等价于
priority_queue<int,vector<int>,less<int> >  q; 

C++中的int,string等类型可以直接比较大小,所以不用我们多操心,优先队列自然会帮我们实现。

  • 但是如果是我们自己定义的结构体,就需要进行重载运算符了

    struct Edge{
        int v1,v2,cost;
        Edge(int tv1,int tv2,int c):v1(tv1),v2(tv2),cost(c){}
        bool operator <(const Edge& e)const{
            return this-> cost > e.cost;
        }
    };
    priority_queue<Node> A;   //默认  大根堆
    priority_queue<Edge, vector<Edge>, less<Edge>>B;    //大根堆
    priority_queue<Edge, vector<Edge>, greater<Edge> > C;    //小根堆
    //或者
    struct cmp {
        bool operator()(Edge a,Edge b) {
            return  a.v1 > b.v1;  //小顶堆
        }
    };
    
    priority_queue<Edge, vector<Edge>, cmp > C;    //小根堆
    
    
  • 没有结构体时也可以按照需要自己定义

    struct cmp{
        bool operator ()(const pair<int,char>& a,const pair<int,char>  &b)
        {
            if(a.first==b.first)return a.second<b.second;
            return a.first>b.first;
        }
    };
    
    int main() {
        //小根堆
        priority_queue<pair<int,char>,vector<pair<int,char>>,cmp > tmp;
        tmp.push({3,'a'});
        tmp.push({3,'b'});
        tmp.push({2,'a'});
        cout<<tmp.top().first<<' '<<tmp.top().second;
    }
    

小根堆声明方式

大根堆是把大的元素放堆顶,小根堆就是把小的元素放到堆顶。

实现小根堆有两种方式:

  • 第一种是比较巧妙的,因为优先队列默认实现的是大根堆,所以我们可以把元素取反放进去,因为负数的绝对值越小越大,那么绝对值较小的元素就会被放在前面,我们在取出的时候再取个反,就用大根堆实现了小根堆。

  • 第二种:

    priority_queue<int,vector<int>,greater<int> >q;
    

    注意,当我们声明的时候碰到两个"<“或者”>"放在一起的时候,一定要记得在中间加一个空格。这样编译器才不会把两个连在一起的符号判断成位运算的左移/右移。

priority_queue容器的使用方法

注意:priority_queue取出队首元素是使用top,而不是front,这点一定要注意!!

7. string

string 并不是STL的一种容器。其实string容器就是个字符串。

string容器的使用方法及与传统字符读入的对比

8. set

set 中的元素是默认排好序(按升序排列)的。set容器自动有序和快速添加、删除的性质是由其内部实现:红黑树(平衡树的一种)。
他可以自定义排序规则

#include <bits/stdc++.h>
struct cmp {
    bool operator()(int x, int y) { return x > y; }
};
struct cmp1 {
    bool operator()(const int &x, const int &y) const { return x > y; }
};
int main() {
    set<int, cmp> st;
    st.insert(5);
    st.insert(3);
    cout << *st.begin() << '\n';
    return 0;
}
set容器的声明

set容器的声明和大部分C++STL容器一样,都是:容器名<变量类型> 名称的结构。

#include<set>
set<int> s;
set<char> s;
set<pair<int,int> > s;
set<node> s;
struct node{...};
set容器的使用
s.empty();//empty()函数返回当前集合是否为空,是返回1,否则返回0.
s.size();//size()函数返回当前集合的元素个数
s.clear();//clear()函数清空当前集合。
s.insert(k);//insert(k)函数表示向集合中加入元素k。
s.begin(),s.end();

begin() 函数和end()函数返回集合的首尾迭代器。注意是迭代器。我们可以把迭代器理解为数组的下标。但其实迭代器是一种指针。这里需要注意的是,由于计算机区间“前闭后开”的结构,begin()函数返回的指针指向的的确是集合的第一个元素。但end()返回的指针却指向了集合最后一个元素后面一个元素。

s.erase(k);

erase(k)函数表示删除集合中元素k。这也反映了set容器的强大之处,完全省略了遍历、查找、复制、还原等繁琐操作。直接一个函数,用O(logn)的复杂度解决问题。

s.find(k);

find(k)函数返回集合中指向元素k的迭代器。如果不存在这个元素,就返回s.end(),这个性质可以用来判断集合中有没有这个元素.

9. multiset

multiset 容器在概念上与set容器不同的地方就是:set的元素互不相同,而multiset的元素可以允许相同。

s.erase(k);

erase(k) 函数在set容器中表示删除集合中元素k。但在multiset容器中表示删除所有等于k的元素。

时间复杂度变成了O(tot+logn),其中tot表示要删除的元素的个数。那么,会存在一种情况,我只想删除这些元素中的一个元素,怎么办呢?可以妙用一下:

if((it=s.find(a))!=s.end())
	s.erase(it);

if中的条件语句表示定义了一个指向一个a元素迭代器,如果这个迭代器不等于s.end(),就说明这个元素的确存在,就可以直接删除这个迭代器指向的元素了.

s.count(k);

count(k)函数返回集合中元素k的个数。set容器中并不存在这种操作。这是multiset独有的。

10. map

map 是“映射容器”,其存储的两个变量构成了一个键值到元素的映射关系

我们可以根据键值快速地找到这个映射出的数据。map容器的内部实现是一棵红黑树(平衡树的一种)。

map容器的用法

因为map容器和set容器都是使用红黑树作为内部结构实现的,所以其用法比较相似。

插入操作
map容器的插入操作大约有两种方法,第一种是类似于数组类型,可以把键值作为数组下标对map进行直接赋值:

mp[1]='a';

当然,也可以使用insert()函数进行插入:

mp.insert(map<int,char>::value_type(5,'d'));

删除操作
可以直接用erase()函数进行删除,如:

mp.erase('b');

遍历操作
和其他容器差不多,map也是使用迭代器实现遍历的。如果我们要在遍历的时候查询键值(即前面的那个),可以用it->first来查询,那么,当然也可以用it->second查询对应值(后面那个)

查找操作
查找操作类比set的查找操作。但是map中查找的都是键值
比如:

mp.find(1);

即查找键值为1的元素。

map和pair的关系

首先,map构建的关系是映射,也就是说,如果我们想查询一个键值,那么只会返回唯一的一个对应值。但是如果使用pair的话,不仅不支持O(log)级别的查找,也不支持知一求一,因为pair的第一维可以有很多一样的,也就是说,可能会造成一个键值对应n多个对应值的情况。这显然不符合映射的概念。

3. vector 使用的注意点

使⽤注意点:

  • 注意插⼊和删除元素后迭代器失效的问题;
  • 清空 vector 数据时,如果保存的数据项是指针类型,需要逐项 delete,否则会造成内存泄漏。

4. vector的 push_back() 和emplace_back()

  • 频繁调⽤ push_back()影响:

    向 vector 的尾部添加元素,很有可能引起整个对象 存储空间的重新分配,重新分配更⼤的内存,再将原数据拷⻉到新空间中,再释 放原有内存,这个过程是耗时耗⼒的,频繁对 vector调⽤ push_back()会导致性能的下降。

  • 在 C++11 之后, vector 容器中添加了新的⽅法: emplace_back() ,和 push_back()⼀样的是都是在容器末尾添加⼀个新的元素进去,不同的是 emplace_back() 在效率上相⽐较于 push_back() 有了⼀定的提升。

  • emplace_back() 函数在原理上⽐ push_back() 有了⼀定的改进,包括在内存优化⽅⾯和运⾏效率⽅⾯。内存优化主要体现在使⽤了就地构造(直接在容器内构造对象,不⽤拷⻉⼀个复制品再使⽤) +强制类型转换的⽅法来实现,在运⾏效率⽅⾯,由于省去了拷⻉构造过程,因此也有⼀定的提升。

5. map 和 set 有什么区别,分别是怎么实现的?

map 和 set 都是 C++ 的关联容器,其底层实现都是红黑树(RB-Tree)。

由于 map 和 set 所开放的各种操作接⼝, RB-tree 也都提供了,所以⼏乎所有的 map 和 set的操作⾏为,都只是转调 RB-tree 的操作⾏为。

map 和 set 区别在于:

  • map 中的元素是 key-value(关键字—值)对:关键字起到索引的作⽤,值则表示与索引相关联的数据; Set与之相对就是关键字的简单集合, set 中每个元素只包含⼀个关键字。

  • set 的迭代器是 const 的,不允许修改元素的值; map允许修改value,但不允许修改key。其原因是因为map和set是根据关键字排序来保证其有序性的,如果允许修改key的话,那么⾸先需要删除该键,然后调节平衡,再插⼊修改后的键值,调节平衡,如此⼀来,严重破坏了map和set的结构,导致iterator失效,不知道应该指向改变前的位置,还是指向改变后的位置。所以STL中将set的迭代器设置成const,不允许修改迭代器的值;⽽map的迭代器则不允许修改key值,允许修改value值

  • map⽀持下标操作, set不⽀持下标操作。 map可以⽤key做下标, map的下标运算符[ ]将关键码作为下标去执⾏查找,如果关键码不存在,则插⼊⼀个具有该关键码和mapped_type类型默认值的元素⾄map中,因此下标运算符[ ]在map应⽤中需要慎用,const_map不能⽤,只希望确定某⼀个关键值是否存在⽽不希望插⼊元素时也不应该使⽤,mapped_type类型没有默认值也不应该使⽤。如果find能解决需要,尽可能⽤find

6. STL 迭代器删除元素:erase函数

  • 对于序列容器 vector, deque来说,使⽤ erase(itertor) 后,后边的每个元素的迭代器都会失效,但是后边每个元素都会往前移动⼀个位置,erase 会返回下⼀个有效的迭代器
  • 对于关联容器 map set 来说,使⽤了 erase(iterator) 后,当前元素的迭代器失效,但是其结构是红⿊树,删除当前元素的,不会影响到下⼀个元素的迭代器,所以在调⽤ erase 之前,记录下⼀个元素的迭代器即可。
  • 对于 list 来说,它使⽤了不连续分配的内存,并且它的 erase ⽅法也会返回下⼀个有效的iterator,因此上⾯两种正确的⽅法都可以使⽤。

7. STL 中迭代器的作用

  • Iterator模式是运⽤于聚合对象的⼀种模式,通过运⽤该模式,使得我们可以在不知道对象内部表示的情况下,按照⼀定顺序(由iterator提供的⽅法)访问聚合对象中的各个元素。
  • 由于Iterator模式的以上特性:与聚合对象耦合,在⼀定程度上限制了它的⼴泛运⽤,⼀般仅⽤于底层聚合⽀持类,如STL的list、 vector、 stack 等容器类及ostream_iterator等扩展iterator。
  • Iterator类的访问方式就是把不同集合类的访问逻辑抽象出来,使得不⽤暴露集合内部的结构⽽达到循环遍历集合的效果。

8. 迭代器和指针的区别

迭代器不是指针,是类模板,表现的像指针。他只是模拟了指针的⼀些功能,通过重载指针的⼀些操作符, ->、 *、 ++、 --等。迭代器封装了指针,是⼀个“可遍历STL( StandardTemplate Library)容器内全部或部分元素”的对象, 本质是封装了原⽣指针,是指针概念的⼀种提升(lift),提供了⽐指针更⾼级的⾏为,相当于⼀种智能指针,他可以根据不同类型的数据结构来实现不同的++, --等操作。迭代器返回的是对象引⽤⽽不是对象的值,所以cout只能输出迭代器使⽤*取值后的值⽽不能直接输出其⾃身

9. STL ⾥ resize 和 reserve 的区别

  • resize():改变当前容器内含有元素的数量(size()), 例如: vector v; v.resize(len); v的size变为len,如果原来v的size⼩于len,那么容器新增(len-size)个元素,元素的值为默认为0。当v.push_back(3);之后,则是3是放在了v的末尾,即下标为len,此时容器的size为len+1
  • reserve():改变当前容器的最⼤容量(capacity) ,它不会⽣成元素,只是确定这个容器允许放⼊多少对象,如果reserve(len)的值⼤于当前的capacity(),那么会重新分配⼀块能存len个对象的空间,然后把之前v.size()个对象通过 copy construtor 复制过来,销毁之前的内存;

C++11 新特性

C++11 的特性主要包括下⾯⼏个⽅⾯:

  • 提⾼运⾏效率的语⾔特性:右值引⽤、泛化常量表达式
  • 原有语法的使⽤性增强:初始化列表、统⼀的初始化语法、类型推导、范围 for 循环、
  • Lambda 表达式、 final 和 override、构造函数委托
  • 语⾔能⼒的提升:空指针 nullptr、 default 和 delete、⻓整数、静态 assert
  • C++ 标准库的更新:智能指针、正则表达式、哈希表等

1. 空指针 nullptr

  • nullptr 出现的⽬的是为了替代 NULL
    在某种意义上来说,传统 C++ 会把 NULL、 0 视为同⼀种东⻄,这取决于编译器如何定义NULL,有些编译器会将 NULL 定 义为 ((void*)0),有些则会直接将其定义为 0。 C++ 不允许直接将 void * 隐式转 换到其他类型,但如果 NULL 被定义为((void*)0),那么当编译 char *ch= NULL; 时, NULL 只好被定义为 0。⽽这依然会产⽣问题,将导致了 C++ 中重载特性 会发⽣混乱,考虑:

    void func(int);
    void func(char *);
    
  • 对于这两个函数来说,如果 NULL ⼜被定义为了 0 那么 func(NULL) 这个语句将 会去调⽤func(int),从⽽导致代码违反直观。

  • 为了解决这个问题, C++11 引⼊了 nullptr 关键字,专⻔⽤来区分空指针、 0。 nullptr 的类型为nullptr_t,能够隐式 的转换为任何指针或成员指针的类型,也能和他们进⾏相等或者不等的⽐较。

  • 当需要使⽤ NULL 时候,养成直接使⽤ nullptr 的习惯。

2. Lambda 表达式

  • Lambda 表达式实际上就是提供了⼀个类似匿名函数的特性,⽽匿名函数则是在需要⼀个函数,但是⼜不想费⼒去命名⼀个函数的情况下去使⽤的。

  • 利⽤ lambda 表达式可以编写内嵌的匿名函数,⽤以替换独⽴函数或者函数对象,并且使代码更可读。

  • 从本质上来讲, lambda 表达式只是⼀种语法糖,因为所有其能完成的⼯作都可以⽤其它稍微复杂的代码来实现,但是它简便的语法却给 C++ 带来了深远的影响。

  • 从⼴义上说, lamdba 表达式产⽣的是函数对象。在类中,可以重载函数调⽤运算符(),此时类的对象可以将具有类似函数的⾏为,我们称这些对象为函数对象(Function Object)或者仿函数(Functor) 。相⽐ lambda表达式,函数对象有⾃⼰独特的优势。

  • lambda lambda 表达式⼀个更重要的应⽤是其可以⽤于函数的参数,通过这种⽅式可以实现回调函数。

  • lambda 表达式⼀般都是从⽅括号[]开始,然后结束于花括号{},花括号⾥⾯就像定义函数那样,包含了 lamdba 表达式体。如果需要参数,那么就要像函数那样,放在圆括号⾥⾯,如果有返回值,返回类型要放在->后⾯,即拖尾返回类型,当然你也可以忽略返回类型, lambda会帮你⾃动推断出返回类型:

    // 指明返回类型,托尾返回类型
    auto add = [](int a, int b) -> int { return a + b; };
    // ⾃动推断返回类型
    auto multiply = [](int a, int b) { return a * b; };
    int sum = add(2, 5); // 输出: 7
    int product = multiply(2, 5); // 输出: 10
    
  • 最前边的 [] 是 lambda 表达式的⼀个很重要的功能,就是 闭包

  • 先说明⼀下 lambda 表达式的⼤致原理:每当你定义⼀个 lambda 表达式后,编译器会⾃动⽣成⼀个匿名类(这个类当然重载了()运算符),我们称为闭包类型(closure type)。那么在运⾏时,这个 lambda 表达式就会返回⼀个匿名的闭包实例,其实是⼀个右值。所以,我们上⾯的 lambda 表达式的结果就是⼀个个闭包实例。

  • 闭包的⼀个强⼤之处是其可以通过传值或者引⽤的⽅式捕捉其封装作⽤域内的变量,前⾯的⽅括号就是⽤来定义捕捉模式以及变量,我们⼜将其称为 lambda 捕捉块。.

    int main() {
    	int x = 10;
    	auto add_x = [x](int a) { return a + x; }; // 复制捕捉x,lambda表达式⽆法修改此变量
    	auto multiply_x = [&x](int a) { return a * x; }; // 引⽤捕捉x, lambda表达式可以修改此变量
    	cout << add_x(10) << " " << multiply_x(10) << endl;
    	// 输出: 20 100
    	return 0;
    }
    

捕获的⽅式可以是引⽤也可以是复制,但是具体说来会有以下⼏种情况来捕获其所在作⽤域中的变量:

  • []:默认不捕获任何变量;
  • [=]:默认以值捕获所有变量;
  • [&]:默认以引⽤捕获所有变量;
  • [x]:仅以值捕获x,其它变量不捕获;
  • [&x]:仅以引⽤捕获x,其它变量不捕获;
  • [=, &x]:默认以值捕获所有变量,但是x是例外,通过引⽤捕获;
  • [&, x]:默认以引⽤捕获所有变量,但是x是例外,通过值捕获;
  • [this]:通过引⽤捕获当前对象(其实是复制指针);
  • [*this]:通过传值⽅式捕获当前对象;

3. 统⼀的初始化语法

  • 不同的数据类型具有不同的初始化语法。如何初始化字符串?如何初始化数组?如何初始化多维数组?如何初始化对象?
  • C++11给出了统⼀的初始化语法:均可使⽤“{}-初始化变量列表”:
X x1 = X{1,2};
X x2 = {1,2}; // 此处的'='可有可⽆
X x3{1,2};
X* p = new X{1,2};
struct D : X {
	D(int x, int y) :X{x,y} {};
};
struct S {
	int a[3];
	// 对于旧有问题的解决⽅案
	S(int x, int y, int z) :a{x,y,z} {};
};

4. 构造函数委托

构造函数可以在同⼀个类中⼀个构造函数调⽤另⼀个构造函数,从⽽达到简化代码的⽬的:

class myBase {
	int number; string name;
	myBase( int i, string& s ) : number(i), name(s){}
public:
	myBase( ) : myBase( 0, "invalid" ){}
	myBase( int i ) : myBase( i, "guest" ){}
	myBase( string& s ) : myBase( 1, s ){ PostInit(); }
};

5. final 和 override

  • C++ 借由虚函数实现运⾏时多态,但 C++ 的虚函数⼜很多脆弱的地⽅:⽆法禁⽌⼦类重写它。可能到某⼀层级时,我们不希望⼦类继续来重写当前虚函数了。容易不⼩⼼隐藏⽗类的虚函数。⽐如在重写时,不⼩⼼声明了⼀个签名不⼀致但有同样名称的新函数。
  • C++11 提供了 final 来禁⽌虚函数被重写/禁⽌类被继承, override 来显示地重写虚函数。 这样编译器给我们不⼩⼼的⾏为提供更多有⽤的错误和警告。
struct Base1 final { };
struct Derived1 : Base1 {}; // 编译错: Base1不允许被继承
struct Base2 {
	virtual void f1() final;
	virtual void f2();
};
struct Derived2 : Base2 {
	virtual void f1(); // 编译错: f1不允许重写
	virtual void f2(int) override; // 编译错:⽗类中没有 void f2(int)
};

6. 哈希表

  • C++ 的 map , multimap , set , multiset 使⽤红⿊树实现, 插⼊和查询都是 O(lgn) 的复杂度,
  • 但 C++11 为这四种模板类提供了(底层哈希实现)以达到 O(1) 的复杂度:
    在这里插入图片描述

7. 智能指针

https://blog.csdn.net/qq_34170700/article/details/107493939

C++11中主要提供三种智能指针:

shared_ptr 、 unique_ptr 、 weak_ptr

auto_ptr是c++98的方案,c++11已经抛弃

为什么要使用智能指针:解决内存泄漏

  • 智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏
  • 使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,析构函数会自动释放资源。
  • 智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间
  • 智能指针包含在头文件<memory>中。

1. shared_ptr

  • shared_ptr实现共享式拥有概念多个智能指针可以指向同一对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用引用计数机制来表明资源被几个指针共享,每一个shared_ptr的拷贝都指向相同的内存。
  • 每使用shared_ptr一次,内部的引用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。shared_ptr内部的引用计数是线程安全的,但是对象的读取需要加锁。
  • shared_ptr 是为了解决auto_ptr在对象所有权上的局限性(auto_ptr是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。
  • shared_ptr会有相互引用死锁的问题:如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。
  • 对shared_ptr进行初始化时不能将一个普通指针直接赋值给智能指针,因为一个是指针,一个是类。可以通过make_shared函数或者通过构造函数传入普通指针。并可以通过get函数获得普通指针。
  • 可以通过成员函数use_count()来查看资源的所有者个数。
  • 除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。
  • 当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。

引用成环例子

#include <iostream>
#include <memory>

class CB;
class CA {
  public:
    CA() {
      std::cout << "CA()" << std::endl;
    }
    ~CA() {
      std::cout << "~CA()" << std::endl;
    }
    void set_ptr(std::shared_ptr<CB>& ptr) {
      m_ptr_b = ptr;
    }
  private:
    std::shared_ptr<CB> m_ptr_b;
};

class CB {
  public:
    CB() {
      std::cout << "CB()" << std::endl;
    }
    ~CB() {
      std::cout << "~CB()" << std::endl;
    }
    void set_ptr(std::shared_ptr<CA>& ptr) {
      m_ptr_a = ptr;
    }
  private:
    std::shared_ptr<CA> m_ptr_a;
};

int main()
{
  std::shared_ptr<CA> ptr_a(new CA());
  std::shared_ptr<CB> ptr_b(new CB());
  ptr_a->set_ptr(ptr_b);
  ptr_b->set_ptr(ptr_a);
  std::cout << ptr_a.use_count() << " " << ptr_b.use_count() << std::endl;

  return 0;
}

成员函数

  • use_count 返回引用计数的个数。
  • unique 返回是否是独占所有权( use_count 为 1)。
  • swap 交换两个 shared_ptr 对象(即交换所拥有的对象)。
  • reset放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少。
  • get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr<int> sp(new int(1)); sp 与 sp.get()是等价的。

make_shared 与 new 的区别

  • new会导致内存碎片化,make_shared则不会。

  • new: 先new后赋值的方式,是先在堆上分配一块内存,然后在堆上再建一个智能指针控制块,这两个东西是不连续的,会造成内存碎片化;

  • make_shared: make_shared的方式是直接在堆上新建一块足够大的内存,其中包含两部分,上面是内存(用来使用),下面是控制块(包含引用计数),然后用T的构造函数去初始化分配的内存。

    shared_ptr<int> p1 = make_shared<int>(42);//安全的内存分配返回指向此对象的shared_ptr
    

2. weak_ptr

https://blog.csdn.net/qq_38410730/article/details/105903979

  • weak_ptr 是一种不影响对象生命周期的智能指针, 它指向一个shared_ptr管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段

  • weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数

  • weak_ptr设计的目的是为配合 shared_ptr而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr或另一个 weak_ptr对象构造, 它的构造和析构不会引起引用记数的增加或减少

  • weak_ptr是用来解决shared_ptr相互引用时的死锁问题(引用成环)。它是对对象的一种弱引用,不会修改对象的引用计数,只能用来检测对象是否已经释放,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

    弱引用能检测到所管理的对象是否已经被释放,从而避免访问非法内存。

引用成环解决方法:以上文的例子来说,解决办法就是将两个类中的一个成员变量改为weak_ptr对象,比如将CB中的成员变量改为weak_ptr对象,即CB类的代码如下:

class CB {
  public:
    CB() {
      std::cout << "CB()" << std::endl;
    }
    ~CB() {
      std::cout << "~CB()" << std::endl;
    }
    void set_ptr(std::shared_ptr<CA>& ptr) {
      m_ptr_a = ptr;
    }
  private:
    std::weak_ptr<CA> m_ptr_a;
};

3. auto_ptr

  • 在C++11之前,auto_ptr 的作用与 unique_ptr 类似:独占所指对象采用所有权模式

  • 在C++11之后,auto_ptr已经被弃用。

  • auto_ptr的构造函数是explicit,阻止了一般指针隐式转换为 auto_ptr的构造,所以不能直接将一般类型的指针赋值给auto_ptr类型的对象,必须用auto_ptr的构造函数创建对象;

  • 由于auto_ptr对象析构时会删除它所拥有的指针,所以使用时避免多个auto_ptr对象管理同一个指针;

  • auto_ptr内部实现,析构函数中删除对象用的是delete而不是delete[],所以auto_ptr不能管理数组;

  • auto_ptr与unique_ptr的区别在于:

    如果尝试对 auto_ptr/unique_ptr 进行拷贝或赋值,unique_ptr 会直接在编译阶段报错;而auto_ptr却可以编译通过,只有在运行阶段,且访问到由于被拷贝而导致失去了对象控制权后变成空指针的auto_ptr后,程序才会报错。

    auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.));
    auto_ptr<string> p2;
    p2 = p1; //auto_ptr不会报错.
    

    此时不会报错,p2剥夺了p1的所有权,所有权转让,p1已经不指向该对象,指向空。当程序运行时访问p1将会报错。
    所以auto_ptr的缺点是:存在潜在的内存崩溃问题

    • 对于特定的对象,只能有一个指针可拥有,这样拥有该对象的智能指针的析构函数会删除该对象,而赋值操作会转让所有权

4. unique_ptr

  • unique_ptr 独占其所指对象,保证同一时间内只有一个智能指针可以指向该对象。

  • 属于跟踪引用和所有权模式

  • 没有一个类似于make_shared的函数用于初始化unique_ptr,unique_ptr只能绑定到一个new返回的指针上。

    unique_ptr<string> p3 (new string ("auto"));   //#4
    unique_ptr<string> p4;                       //#5
    p4 = p3;//此时会报错!!
    

    编译器认为p4=p3非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全

  • 另外unique_ptr还有更聪明的地方:当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr将存在一段时间,编译器将禁止这么做

    unique_ptr<string> pu1(new string ("hello world"));
    unique_ptr<string> pu2;
    pu2 = pu1;                                      // #1 not allowed
    unique_ptr<string> pu3;
    pu3 = unique_ptr<string>(new string ("You"));   // #2 allowed
    
  • 如果确实想执行类似与#1的操作,要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。例如:

    unique_ptr<string> ps1, ps2;
    ps1 = demo("hello");
    ps2 = move(ps1);
    ps1 = demo("alexia");
    cout << *ps2 << *ps1 << endl;
    
  • unique_ptr指针本身的生命周期:从unique_ptr指针创建时开始,直到离开作用域。离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete操作符,用户可指定其他操作)。

8. C++类型转换

C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。

C++类型转换主要分为两种:隐式类型转换、显式类型转换(强制类型转换)。

所谓隐式类型转换,是指不需要用户干预,编译器默认进行的类型转换行为。C++中提供了explicit关键字,在构造函数声明的时候加上explicit关键字,能够禁止隐式转换。

强制类型转换操作符有四种:static_castconst_castreinterpret_castdynamic_cast

1. static_cast 静态类型转换

所谓的静态,即在编译期内即可决定其类型的转换,用的也是最多的一种。它主要有如下几种用法:

  • 用于类层次结构中父类和子类之间指针或引用的转换。进行向上转换(把子类的指针或引用转换成父类表示)是安全的;
  • 进行向下转换(把父类指针或引用转换成子类指针或引用)时,由于没有动态类型检查,所以是不安全的;
  • 用于基本数据类型之间的转换。这种转换的安全性也要开发人员来保证。
    	int val_int;
    	double val_double = 3.1415;
    	val_int = static<int>(val_double);
    
  • 把void指针转换成目标类型的指针不安全
  • 把任何类型的表达式转换成void类型。

2. const_cast

  • 专⻔⽤于 const 属性的转换,是四个转换符中唯⼀⼀个可以操作常量的转换符。

  • const_cast运算符用来去掉类型的const或volatile属性。但需要特别注意的是const_cast不是用于去除变量的常量性,而是去除指向常数对象的指针或引用的常量性,其去除常量性的对象必须为指针或引用。

  • 对于将常量对象转换成非常量对象的行为,我们一般称其为“去掉const性质(cast away the const)”。一旦我们去掉了某个对象的const性质,编译器就不再阻止我们对该对象进行写操作了。

    const char* ptr_char;
    char* p = const_cast<char*>(ptr_char); // 正确:但是通过p写值的行为是未定义的行为
    

3. reinterpret_cast 重新解释类型转换

  • 不到万不得已,不要使⽤这个转换符,⾼危操作。
  • 使⽤特点: 从底层对数据进⾏重新解释,依赖具体的平台,可移植性差; 可以将整形转 换为指针,也可以把指针转换为数组;可以在指针和引⽤之间进⾏肆⽆忌惮的转换。

4. dynamic_cast 子类与父类之间的多态类型准换

  • dynamic_cast 主要用在继承体系中的安全向下转型。它能安全地将指向基类的指针转型为指向子类的指针或引用,并获知转型动作成功是否。转型失败会返回null(转型对象为指针时)或抛出异常bad_cast(转型对象为引用时)。
  • 和static_cast不同,dynamic_cast涉及运行时的类型检查。如果向下转型是安全的(也就是说,如果基类指针或者引用确实指向一个派生类的对象),这个运算符会传回转型过的指针。如果向下转型不安全(即基类指针或者引用没有指向一个派生类的对象),这个运算符会传回空指针。
  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值