目录

[C++基础]关键字与运算符

指针与引用

define 和 typedef 的区别

define 和 inline 的区别

override 和 overload

new 和 malloc

constexpr 和 const

volatile

extern

static

前置++ 和 后置++

C++三大特性

访问权限

1. 继承

2. 封装

3.  多态

虚函数

空类

抽象类与接口的实现

智能指针

1. shared_ptr

2. unique_ptr

3. weak_ptr

C++强制类型转换

1. static_cast

2. dynamic_cast

3. reinterpret_cast

4. const_cast

C++内存模型

字符串操作函数

内存泄漏

测试题目

1. 以下为WindowsNT 32位C++程序,请计算下面sizeof的值

2. 分析下面Test函数会有什么样的结果

3. 实现内存拷贝函数

4. 假如考虑dst和src内存重叠的情况,strcpy该怎么实现

5. 按照下面要求写程序

6. 说一说进程的地址空间分布

7. 说一说C与C++的内存分布方式

8. new、delete、malloc、free关系

计算机中的乱序执行

副作用

信号量

1.binary_semaphore

2. counting_semaphore

future库

运算符重载

不建议重载:

建议非成员:

函数调用运算符:

[C++ STL]

STL实现原理机器实现

容器

算法

迭代器

仿函数

适配器

空间配置器

STL的优点

pair容器

vector容器实现与扩充

1. 底层实现

2. 扩容过程

3. vector源码

list-链表

list设计

vector 和 list 的区别

deque-双端数组

1. deuqe概述

2. deque中控器

stack 和 queue

源码

heap and priority_queue

heap(堆):

priority_queue:

map && set

3. 细节

map与unordered_map

底层实现

map

unordered_map

《Effective STL》

1. 慎重选择容器类型

2. 不要试图编写独立于容器的代码

3. 确保容器中的对象拷贝正确且高效

4. 调用empty而不是检查size()是否为0

5. 区间成员函数优先于之对应的单元素成员函数

6. 当心C++编译器的烦人的分析机制-尽可能的解释为函数声明

7.容器包含指针

8. 切勿创建包含auto_ptr的容器

9. 当你复制一个auto_ptr时

10. 慎重选择删除元素的方法

10.1 要删除容器中有特定值的所有对象

10.2 要在循环内部做某些(除了删除对象的操作之外)操作

11. 了解分配子的约定与概念

1. 首先分配子能够为它所定义的内存模型中的指针和引用提供类型的定义

2. 库实现者可以忽略类型定义而直接使用指针和引用

3. STL实现者可以假定所有属于同一类型的分配子都是等价的

4. 大多数标准容器从来没有单独使用过对应的分配子

12.编写自定义分配子需要什么?

13. 理解分配子的用法

14. 切勿对STL容器的线程安全性有不切实际的依赖

15. 当你在动态分配数组的时候,请使用vector和string

16. 使用reserve来避免不必要的重新分配

有两种方式避免不必要的内存分配

17. string实现的多样性

18. 了解如何把vector和string数据传给旧的API

19. 使用“swap技巧”删除多余的容量

20. swap的时候发生了什么

21. 避免使用vect,用deque和bitset代替它

22. 理解等价与相等

23. 熟悉非标准散列容器

24. 为包含指针的关联容器指定比较类型,而不是比较函数,最好是准备一个模板

25. 切勿直接修改set或multiset中的键

26.考虑用排序的vector替代关联容器

27. 更新一个已有的映射表元素

28.  iterator

29.使用distance和advance将容器的const_iterator转换为iterator

30. 正确理解由reserve_iteratorr的base()成员函数所产生的iterator的用法

31. istreambuf_iterator

32. 如果使用的算法需要指定一个目标空间

33. 了解各种与排序相关的选择

34. 如果要删除元素,需要在remove后面使用erase

35.对包含指针的容器使用remove这一类算法一定要小心

36. 了解那些算法要求使用排序的区间作为参数

37. 使用accumulate或者for_each进行区间统计

38. 遵循按值传递的原则来设计函数子类

41. 确保判别式是“纯函数”

42. 使你的函数子类可配接

43. 理解ptrfun && memfun && memfunref

44. 确保less与operator<的语义相同

45. 算法的调用优先于手写的循环

46. 容器的成员函数优先于同名函数

47. 正确区分以下关键字:

48. 使用函数对象作为STL算法的参数

49.避免产生“直写行”的代码

50. 包含正确的头文件

51. 学会分析于STL相关的编译器的诊断信息

52. 熟悉于STL相关的web站点

《Effective C++》

条款16:成对使用new和delete。

条款30:了解inline的里里外外

条款34:区分接口继承和实现继承

条款35:考虑virtual函数以外的选择

条款36:绝不重新定义继承而来的非虚函数

条款37:绝不重新定义继承而来的缺省参数值

条款39:明智而谨慎的使用private继承

条款40:明智而谨慎的使用多重继承

条款44:将与参数无关的代码抽离template

条款45:运用成员函数模板接受所有兼容类型

条款46:需要类型转换时请以模板定义非成员函数

条款47:请使用traits classes表现类型信息

条款48:认识template元编程

泛型编程

C++模板全特化和偏特化

对函数模板:

模板函数:

对类模板:

C++11新特性

1. 类型推导

1. auto

2. decltype

auto和decltype的配合使用

2. 右值引用

1. 将亡值

2. 左值引用

3. 右值引用

4. 移动语义

5. 完美转发

nullptr

范围for循环

列表初始化

lambda表达式

1. lambda表达式的用法:

2. lambda表达式的特点:

并发

1. std::thread

2. lock_guard

3. unique_lock


[C++基础]
关键字与运算符

指针与引用

1. 指针存放某个对象的地址,其本身是变量(命名了的对象),本身就有地址,所以可以有指向指针的指针;可变,包括其所指向地址的改变和棋指向的地址中所存放的数据的改变

2. 引用就是变量的别名,从一而终,不可变,必须初始化

3.不存在指向空的值的引用,但是存在指向空值的指针

define 和 typedef 的区别

define:

1. 只是简单的字符串替换,没有类型检查

2. 是在编译的预处理阶段起作用

3. 可以用来防止头文件重复引用

4. 不分配内存,给出的是立即数,有多少次使用就进行多少次替换

typedef:

1. 有对应的数据类型,要进行判断

2. 是在编译、运行的时候起作用

3. 在静态存储区中分配空间,在程序运行过程中内存只有一个拷贝

define 和 inline 的区别

1. define:

定义预编译时处理的宏,只是简单的字符串替换,无类型检查,不安全。

2. inline:

inline是先将内联函数编译完成生成了函数体直接插入被调用的地方,减少了压栈,跳转和返回的操作。没有普通函数调用额外的开销;

内联函数是一种特殊的函数,会进行类型检查;

对编译器的一种请求,编译器有可能拒绝这种请求;

C++中inline编译限制:

1. 不能存在任何形式的循环语句

2. 不能存在过多的条件判断语句

3. 函数体不能过于庞大

4. 内联函数声明必须在调用语句之前

override 和 overload

1. override 是重写(覆盖)了一个方法

以实现不同的功能,一般是用于子类在继承父类的时候,重写父类方法。

规则:

1. 重写方法的参数列表,返回值,所抛出的异常与被重写方法一致

2. 被重写的方法不能为private

3. 静态方法不能被重写为非静态的方法

4. 重写方法的访问修饰符一定要大于被重写方法的访问修饰符

2. overload 是重载,这些方法的名称相同而参数不同

一个方法有不同的版本,存在于一个类中。

规则:

1. 不能通过访问权限,访问类型,抛出的异常进行仲裁

2. 不同的参数类型可以是不同的参数类型,不同的参数个数,不同的参数顺序(参数类型必须不一样)

3. 方法的异常类型和数目不会对重载造成影响

使用多态是为了避免在父类里大量重载引起代码臃肿且难以维护。

重写与重载的本质区别是,加入了override的修饰符的方法,此方法始终只有一个被使用的方法。

new 和 malloc

1. new分配内存失败时,会抛出bac_alloc异常,不会返回NULL;malloc分配内存失败会返回NULL。

2. 使用new操作符申请内存分配时无需指定内存块的大小,而malloc则需要显式地指出所需内存的尺寸。

3. operato new / operator delete 可以被重载,而malloc不允许重载。

4. new/delete会调用对象的构造函数/析构函数完成对象的构造/析构,malloc不会

5. malloc 和 free是C++/C语言的标准库函数,new/delete是C++的运算符

6. new操作符从自由存储区上为对象动态分配内存空间,malloc函数从堆上动态分配内存

表格

new/delete malloc/free
本质属性 运算符 CRT函数
内存分配大小 自动计算 手动计算
类型安全 是(一个int类型指针指向float会报错) 不是(malloc类型转换成int,分配double数据类型大小的内存空间不会报错)
两者关系 new封装了malloc
其他特点 除了分配和释放内存还回调用构造和析构函数 只分配和释放内存
内存分配失败会抛出bad_alloc异常 内存分配失败时会返回null
返回定义时具体类型的指针 返回void类型指针,使用时需要类型转换

constexpr 和 const

const表示“只读”的语义,constexpr 表示“常量”的语义

constexpr 智能定义编译期常量,而const可以定义编译期常量,也可以定义运行期常量。

将一个成员函数标记为constexpr,则顺带也将其标记为const。如果将一个变量标记为constexpr,则也是const的。但是相反不成立。

constexpr变量

复杂系统中很难分辨一个初始值是不是常量表达式,可以将变量声明为constexpr类型,由编译器来验证变量的值是否是一个常量表达式。

必须用常量初始化:

constexpr int n = 20;
constexpr int m = n + 1;
static constexpr int MOD = 1000000007;

如果constexpr声明中定义了一个指针,constexpr仅对指针有效,与对象无关。

constexpr int *p = nullptr; //常量指针 顶层const
const int *q = nullptr; //指向常量的指针, 底层const
int *const q = nullptr; //顶层const

constexpr函数

constexpr函数是指能用于常量表达式的函数。

函数的返回类型和所有形参类型都是字面值类型,函数体有且只有一条return语句。

constexpr int new() {return 42;}

为了可以在编译过程中展开,constexpr函数被隐式地转换成了内联函数。

constexpr和内联函数可以在程序中多次定义,一般定义在头文件。

constexpr构造函数

构造函数不能说const,但字面值常量类的构造函数可以是constexpr。

constexpr构造函数必须有一个空的函数体,即所有成员变量的初始化都放到初始化列表中。对象调用的成员函数必须使用constexpr修饰

const

指针常量:const int* d = new int(2);

常量指针:int *const d = new int(2);

区别方法:

左定值,右定向:指的是const在*的左边还是右边

拓展:

顶层const:指针本身是常量;

底层const:指针所指的对象是常量;

若要修改const成员函数中某些与类状态无关的数据成员,可以使用mutable关键字来修饰这个数据成员;

const 和 static 的区别

关键字 修饰常量【非类中】 修饰成员变量 修饰成员函数
const

超出其作用域后空间会被释放;

在定义时必须初始化,之后无法改变;

const形参可以接受const和非const类型的实参

只在某个对象的声明周期内是常量,而对整个对象而言是可变的;

不能赋值,不能在类外定义;只能通过构造函数的参数初始化列表初始化【原因:因为不同的对象对其const数据成员的值可以不同,所以不能在类中声明时初始化】

防止成员函数修改对象的内容【不能修改成员变量的值,但是可以访问】

const对象不可以调用非const的函数;但是非const对象可以调用;

static 在执行函数后不会释放其存储空间 只能用在类定义体内部的声明,外部初始化,且不加static

1. 作为类作用域的全局函数【不能访问非静态数据成员和调用非静态成员函数】

2. 没有this指针【不能直接存取非类的非静态成员,调用非静态成员函数】

3. 不能声明为virtual

const和static不能同时修饰成员函数,原因:静态函数不含有this指针,即不能实例化,而const成员函数必须具体到某一实例

constexpr的好处:

1. 为一些不能修改数据提供保障,写成变量则就有被意外修改的风险。

2. 有些场景,编译器可以在编译期对constexpr的代码进行优化,提高效率。

3. 相比宏来说,没有额外的开销,但更安全可靠。

volatile

定义:

[与const绝对对立的,是类型修饰符] 影响编译期编译的结果,用该关键字声明的变量表示该变量随时可能发生变化,与该变量有关的运算,不要进行编译优化;会从内存中重新装载内容,而不是直接从寄存器拷贝内容。

作用:

指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值,保证对特殊地址的稳定访问

使用场合:

在中断服务程序和cpu相关寄存器的定义

举例说明:

for(volatile int i=0; i<100000; i++); // 它会执⾏,不会被优化掉

extern

定义:声明外部变量【在函数或者文件外部定义的全部变量】

static

作用:实现多个对象之间的数据共享和隐藏,并且使用静态成员还不会破坏隐藏规则,默认初始化为0

前置++ 和 后置++

self &operator++() {node = (linktype)((node).next);return *this; }const self operator++(int) {self tmp = *this;++*this;return tmp; }

为了区分前后置,重载函数是以参数类型来区分,在调用的时候,编译器默认给int指定为

01. 为什么后置返回对象,而不是引用?

因为后置为了返回旧值创建了一个临时对象,在函数结束的时候这个对象就会被销毁,如果返回引用,那么引用也会因为对象的销毁而销毁。

02. 为什么后置前面也要加const?

可以不加,但是为了防止使用I++++,连续两次调用后置++重载符

原因:
与内置类行为不一致;无法获得所期望的结果,因为第一次返回的是旧值,调用两次后置++,结果只累加了一次,必须手动禁止其合法化,就要在前面加上const。

03. 处理用户的自定义类型

最好使用前置++,因为它不会创建临时对象,进而不会带来构造和析构造成的额外开销。

std::atomic

问题:a++ 和 int a = b 在C++中是否是线程安全的?

答案:不是

例1:
a++:此部分C/C++语法的级别来看,这是一条语句,应该是原子的;但从编译器得到的汇编指令来看,不是原子的。

其一般对应三条指令,首先将变量a对应的内存值搬运到某个寄存器(如eax)中,然后将该寄存器中的值自增1,再将该寄存器中的值搬运回a代表的寄存器中

mov eax, dword ptr [a]  # (1)
inc eax                 # (2)
mov dword ptr [a], eax  # (3)
现在假设i的值是0,有两个线程,每个线程对变ᰁa的值都递增1,预想⼀下,其结果应该是2,可实际运⾏结构可能是1!是不是很奇怪?
分析如下:
int a = 0;
// 线程1(执⾏过程对应上⽂汇编指令(1)(2)(3))
void thread_func1() {a++;
}
// 线程2(执⾏过程对应上⽂汇编指令(4)(5)(6))
void thread_func2() {a++;}
我们预想的结果是线程1和线程2的三条指令各⾃执⾏,最终a的值变为2,但是由于操作系统线程调度的不确定性,线程1执⾏完指令(1)和(2)后,eax寄存器中的值变为1,此时操作系统切换到线程2执⾏,执⾏指令(3)(4)(5),此时eax的值变为1;接着操作系统切回线程1继续执⾏,执⾏指令(6),得到a的最终结果1。

例2:

int a = b; 从C/C++语法的级别来看,这条语句应该是原子的;但从编译器得到的汇编指令来看,由于现在计算机CPU架构体系的限制,数据不能直接从内存某处搬运到内存另外一处,必须借助寄存器中转,因此这条语句一般对应两条计算机指令,即将变量b的值搬运到某个寄存器中,再从该寄存器搬运到变量a的内存地址中:

mov eax, dword ptr [b]
mov dword prt [a], eax

既然是两条指令,那么多个线程在执行这两条指令时,某个线程可能会在第一条指令执行完毕后被剥夺CPU时间片,切换到另一个线程出现而不确定的情况。

解决办法:C++11新标准发布后改变了这种困境,新标准提供了对整形变量原子操作的相关库,即std::atomic,这是一个模板类型:

template<class T>
struct atomic;

我们可以传入具体的整型类型对模板进行实例化,实际上stl库也提供了这些实例化的模板类型

// 初始化1
std::atomic<int> value;
value = 99;// 初始化2
// 下⾯代码在Linux平台上⽆法编译通过(指在gcc编译器)
std::atomic<int> value = 99;
// 出错的原因是这⾏代码调⽤的是std::atomic的拷⻉构造函数
// ⽽根据C++11语⾔规范,std::atomic的拷⻉构造函数使⽤=delete标记禁⽌编译器⾃动⽣成
// g++在这条规则上遵循了C++11语⾔规范。

C++三大特性

访问权限

C++通过public、protected、private三个关键字来控制成员变量和成员函数的访问权限,它们分别表示公有的、受保护的、私有的,被称为成员访问限定符。

在类的内部(定义类的代码内部),无论成员被声明为public,protected还是private,都是可以互相访问的,没有访问权限的限制。

在类的外部(定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问public成员,不能访问private、protected属性的成员。

无论公有继承、私有和保护继承,私有成员不能被“派生类”访问,基类中的公有和保护成员能被“派生类”访问。

对于公有继承,只有基类中的公有成员能被“派生类”访问,保护和私有成员不能被“派生类对象”访问。对于私有和保护继承,基类中的所有成员不能被“派生类对象”访问。

1. 继承

定义:

让某种类型对象获得另一个类型对象的属性和方法

功能:

它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行拓展

常见的继承有三种方式:

1. 实现继承:指使用基类的属性和方法而无需额外编码的能力

2. 接口继承:指仅使用属性和方法的名称,但是子类必须提供实现的能力

3. 可视继承:指子窗体(类)使用基窗体(类)的外观和实现代码的能力

例如:

将⼈定义为⼀个抽象类,拥有姓名、性别、年龄等公共属性,吃饭、睡觉等公共⽅法,在定义⼀个具体的⼈时,就可以继承这个抽象类,既保留了公共属性和⽅法,也可以在此基础上扩展跳舞、唱歌等特有⽅法。

2. 封装

定义:

数据和代码捆绑在一起,避免外界干扰和不确定性访问;

功能:

把客观事务封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或对象操作,对不可信的进行信息隐藏,例如:将公共的数据或方法使用public修饰,而不希望被访问的数据或方法用private修饰。

3.  多态

定义:

同一事物表现出不同事物的能力,即向不同对象发送同一消息,不同的对象在接收时会产生不同的行为(重载实现编译时多态,虚函数实现运行时多态)

功能:

多态性是允许你将父对象设置成为和一个或更多的他的子对象相等的技术,赋值之后,父对象就可以根据当前赋值给他的子对象的特性以不同的方式运作。

简单概括:允许将子类类型的指针赋值给父类类型的指针。

实现多态的两种方式:

1. 覆盖(override):指子类重新定义父类的虚函数的做法。

2. 重载(overload):指允许存在多个同名函数,而这些函数的参数表不同(或许参数个数不同,或许参数类型不同,或许都不同)

例如:
基类是⼀个抽象对象——⼈,那学⽣、运动员也是⼈,⽽使⽤这个抽象对象既可以表示学⽣、也可以表示运动员。

虚函数

当基类希望派生类定义适合自己的版本,就将这些函数声明为虚函数(virtual)。

虚函数依赖虚函数表工作,表来保存虚函数的地址,当我们用基类指针指向派生类时,虚表指针指向派生类的虚函数表。

这个机制可以保证派生类中的虚函数被调用到。

1. 虚函数是动态绑定的

也就是说,使用虚函数的指针和引用能够正确找到实际类的对应函数,而不是执行定义类的函数,这是虚函数的基本功能。

2. 多态(不同继承关系的类对象,调用同一函数产生不同行为)

1. 调用函数的对象必须是指针或者引用

2. 被调用的函数必须是虚函数(virtual),且完成了虚函数的重写(派生类中有一个跟基类的完全相同虚函数)

3. 动态绑定绑定的是动态类型

所对应的函数或属性依赖于对象的动态类型,发生在运行期。

4. 构造函数不能是虚函数

而且,在构造函数中调用虚函数,实际执行的是父类的对应函数,因为自己的还没有构造好,多态是被disable的。

5. 虚函数的工作方式

依赖虚函数表工作的,表来保存虚函数地址,当我们用基类指针指向派生类时,虚表指针vptr指向派生类的虚函数表。这个机制可以保证派生类中的虚函数被调用到。

6. 析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必须的。

7. 将一个函数定义为纯虚函数

实际上是将这个类定义为抽象类,不能实例化对象;纯虚函数通常没有定义体,但完全可以拥有。

8. inline,static,constructor三种函数都不能带virtual关键字。

(1)inline是在编译时展开必须要有实体;

内联函数是指在编译期间用被调用函数体本身来代替函数的调用指令,但虚函数的多态性需要在运行时根据对象的类型才知道调用哪个虚函数,所以没法在编译时进行内联函数展开。

(2)static属于class自己的类相关,必须有实体;

static成员没有this指针。virtual函数一定要通过对象来调用,有隐藏的this指针,实例相关。

9. 析构函数可以是纯虚的

但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。

10. 派生类的override虚函数定义必须和父类完全一致。

除了一个特例,如果父类中返回值是一个指针或引用,子类override时可以返回这个指针(或引用)的派生。

为什么要虚继承?

1. 为了解决多继承命名冲突和冗余数据问题

C++提出了虚继承,使得派生类中只保留一份间接基类的成员。其中多继承(Multiple inheritance)是指从多个直接基类中产生派生类的能力,多继承的派生类继承了所有父类的成员。

2. 虚继承的目的是让某个类做出声明,承诺愿意共享它的基类

其中,这个被共享的基类就称为虚基类(Virtual Base Class),其中A就是一个虚基类。在这种机制下,不论虚基类A在继承体系出现了多少次,在派生类中都只包含一份虚基类的成员。

类A有一个成员变量a,不使用虚继承,那么在类D中直接访问a就会产生歧义。

编译器不知道它究竟来自哪个路径。

C++标准库中的iostream类就是一个虚继承的实际应用案例。

iostream从istream和ostream直接继承而来,而istream和ostream又都继承自一个共名的baseios的类,是典型的菱形继承。

此时istream 和 ostream 必须采用虚继承,否则将导致iostream类中保留两份baseios类的成员。

使用多继承经常出现二义性,必须小心;

一般只有在比较简单和不易出现二义性或者实在必要的情况下才使用多继承,能用单一继承解决的问题就不用多继承。

空类

1. 为何空类的大小不是0

为了确保两个不同对象的地址不同,必须如此。

类的实例化是在内存中分配一块地址,每个实例在内存中都有独一无二的二地址。

同样,空类也会实例化,所以编译器会给空类隐含的添加一个字节,这样空类实例化后就有独一无二的地址了。所以空类的sizeof是1,而不是0。

何时空想虚函数地址表:

如果派生类继承的第一个是基类,且该基类定义了虚函数的地址表,则派生类就共享该表首地址占用的存储单元。

对于除前述情形以外的其他任何情形,派生类在处理完所有基类或者虚类后,根据派生类是否建立了虚函数地址表,确定是否为该表首址分配内存单元。

class X{}; //sizeof(X):1
class Y : public virtual X {}; //sizeof(Y):4
class Z : public virtual X {}; //sizeof(Z):4
class A : public virtual Y {}; //sizeof(A):8
class B : public Y, public Z{}; //sizeof(B):8
class C : public virtual Y, public virtual Z {}; //sizeof(C):12
class D : public virtual C{}; //sizeof(D):16

抽象类与接口的实现

接口描述了类的行为和功能,而不需要完成类的特定实现;C++接口是使用抽象类来实现的

1. 类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用“=0”来指定的。

2. 设计抽象类(通常称为ABC)的目的,是为了给其他类提供一个可以继承的适当的基类。抽象类不能被用于实例化对象,只能作为接口使用。

class Shape
{
public:// 提供接⼝框架的纯虚函数virtual int getArea() = 0;void setWidth(int w){width = w;}void setHeight(int h){height = h;}
protected:int width;int height;
};
// 派⽣类
class Rectangle: public Shape
{
public:int getArea(){return (width * height);}
};
class Triangle: public Shape
{
public:int getArea(){return (width * height)/2;}
};//主函数:
Rectangle Rect;
Triangle Tri;
Rect.setWidth(5);
Rect.setHeight(7);
Rect.getArea(); //35
Tri.setWidth(5);
Tri.setHeight(7);
Tri.getArea(); //17

智能指针

1. shared_ptr

1. shared_ptr的实现机制是在拷贝构造时使用同一份引用计数

(1)一个模板指针T* ptr

指向实际的对象

(2)一个引用次数

必须new出来的,不然会多个shared_ptr里面会有不同的引用次数而导致多次delete

(3)重载operator* 和 operator ->

使得能像指针一样使用shared_ptr

(4)重载copy constructor

使其引用次数加一(拷贝构造函数)

(5)重载operator=(赋值运算符)

如果原来的shared_ptr已经由对象,则让其引用次数减一并判断引用是否为零(是否调用delete),然后将新的引用次数加一

(6)重载析构函数

使引用次数减一并判断引用是否为零;(是否调用delete)

2. 线程安全问题

(1)同一个shared_ptr被多个线程“读”是安全的;

(2)同一个shared_ptr被多个线程“写”是不安全的

证明:在多个线程中同时对一个shared_ptr循环执行两边swap。shared_ptr的swap函数的作用就是和另一个shared_ptr交换引用对象和引用计数,是写操作。执行两遍swap后,shared_ptr引用对象的值应该不变

(3)共享引用计数的不同的shared_ptr被多个线程“写”是安全的。

2. unique_ptr

1. unique_ptr“唯一”拥有其所指对象

同一时刻只能有一个unique_ptr指向给定对象,离开作用域时,若其指向对象,则将其所指对象销毁(默认delete)。

2. 定义unique_ptr时

需要将其绑定在一个new返回的指针上。

3. unique_ptr不支持普通的拷贝和赋值(因为拥有所指对象)

但是可以拷贝和赋值一个将要被销户的unique_ptr;可以通过release或者reset将指针所有权从一个非const的unique_ptr转移到另一个unique。

3. weak_ptr

1. weak_ptr是为了配合shared_ptr而引入的一种智能指针

它的最大作用在于协助shared_ptr工作,像旁观者那样观测资源的使用情况,但weak_ptr没有共享资源,它的构造不会引起指针引用计数的增加。

2. 和shared_ptr指向相同内存

shared_ptr析构之后内存释放,在使用之前使用挂表示公有lock()检查weak_ptr是否为空指针。

C++强制类型转换

关键字:static_cast 、dynamic_cast、reinterpret_cast 和 const_cast

1. static_cast

没有运行时类型检查来保证转换的安全性

进行上行转换(把派生类的指针或引用转换成基类的表示)是安全的

进行下行转换(把基类的指针或引用转换为派生类的表示),没有动态类型检查,是不安全的。

使用:

1. 用于基本数据类型之间的转换,如把int转换成char。

2. 把任何类型的表达式转换成void类型。

2. dynamic_cast

在下行转换时,dynamic_ptr具有类型检查(信息在虚函数中)的功能,比static_cast更安全。

转换后必须是类的指针、引用或者void*,基类要有虚函数,可以交叉转换。

dynamic本身只能用于存在虚函数的父子关系的强制类型转换;对于指针,转换失败则返回nullptr,对于引用,转换失败会抛出异常。

3. reinterpret_cast

可以将整型转换为指针,也可以把指针转换为数组;可以在指针和引用里进行肆无忌惮的转换,平台移植性价比差。

4. const_cast

常量指针转换为非常量指针,并且仍然指向原来的对象。常量引用被转换为非常量引用,并且仍然指向原来的对象。去掉类型的const或volatile属性。

C++内存模型

字符串操作函数

常见的字符串函数实现

1. strcpy()

把从strsrc地址开始且含有“\0”结束符的字符串复制到从strdest开始的地址空间,返回类型为char*

char *strcpy(char *strDest, const char *strSrc)
{assert((strDest != NULL) && (strSrc != NULL));char *address = strDest;while((*strDest++ = *strSrc++) != '\0');return address;
}

2. strlen()

计算给定字符串的长度。

int strlen(const char *str)
{assert(str != NULL);//断言字符串地址非0int len;while((*Str++) != '\0'){len++;}return len;
}

3. strcat()

作用是把src所指字符串添加到dest结尾处。

char *strcat(char *dest, const char *src)
{assert(dest && src);char *ret = dest;//找到dest的'\0'结尾符while(*dest){dest++;}//拷贝(while循环退出时,将结尾符'\0'也做了拷贝)while(*dest++ = *src++) {}return ret;
}

4. strcmp()

比较两个字符串设这两个字符串为str1, str2,

若str == str2,则返回零

若str1 < str2,则返回负数

若str1 > str2,则则返回正数

int strcmp(const char *str1, const char *str2)
{assert(str1 && str2);//找到首个不相等的字符while(*str1 && *str2 && (*str1 == *str2)){str1++;str2++;      }return *str1 - *str2;
}

内存泄漏

1. 什么是内存泄漏?

内存泄漏(memory leak)是指由于疏忽或错误造成了程序未能释放掉不再使用的内存的情况。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

可以使用Valgrind,mtrace进行内存泄漏检查。

2. 内存泄漏的分类

(1)堆内存泄漏(Heap leak)

堆内存指的是程序运行中根据需要分配通过malloc,realloc new等从堆中分配的一块内存,再是完成后必须通过调用对应的free或者delete删掉。如果程序的设计的错误导致这部分内存没有被释放,那么此后这款亚欧内存将不会被使用,就会产生heap leak。

(2)系统资源泄漏(Resource Leak)

主要指程序使用系统分配的资源比如Bitmap, handle, SOCKET等没有使用相应的函数释放掉,导致系统资源的浪费,严重可导致系统效能降低,系统运行不稳定。

(3)没有将基类的析构函数定义为虚函数

当基类指针指向子类对象时,如果基类的析构函数不是virtual,那么子类的析构函数将不会被调用,子类的资源没有正确释放,因此造成内存泄漏。

3. 什么操作会导致内存泄漏?

指针指向改变,未释放动态分配内存。

4. 如何防止内存泄漏?

将内存的分配封装在类中,构造函数分配内存,析构函数释放内存;使用智能指针

5. 智能指针的了解有哪些?
智能指针是为了解决动态分配内存导致内存泄漏和多次释放同一内存所提出的。C++标准中放在<memory>头文件。包括:共享指针,独占指针,弱指针

6. 构造函数,析构函数要设为虚函数吗,为什么?

(1)析构函数

需要。当派生类对象中有内存需要回收时,如果析构函数不是虚函数,不会触发动态绑定,只会调用基类虚构函数,导致派生类资源无法释放,造成内存泄漏。

(2)构造函数

不需要。没有意义。虚构函数调用是在部分信息下完成工作的机制,允许我们只知道接口而不知道对象的确切类型。要创建一个对象,需要知道对象的完整信息。特别是,需要知道要创建的确切类型。因此,构造函数不应该被设定为虚函数。

测试题目

1. 以下为WindowsNT 32位C++程序,请计算下面sizeof的值

char str[] = "hello";
char *p = str;
int n = 10;
//请计算
sizeof(str) = ?; //6,是数组的所占内存的⼤⼩包括末尾的 '\0'
sizeof(p) = ?;   //4, p为指针变ᰁ,32位系统下⼤⼩为 4 bytes
sizeof(n) = ?;   //4,n 是整型变ᰁ,占⽤内存空间4个字节void Func(char str[100]){// 请计算sizeof(str) = ?; //4,函数的参数为字符数组名,即数组⾸元素的地址,⼤⼩为指针的⼤⼩}
void* p = malloc(100);
// 请计算
sizeof(p) = ?; //4,p指向malloc分配的⼤⼩为100 byte的内存的起始地址,sizeof(p)为指针的⼤⼩,⽽不是它指向内存的⼤⼩

2. 分析下面Test函数会有什么样的结果

void GetMemory1(char* p)
{p = (char*)malloc(100);
}//程序崩溃。 因为GetMemory1并不能传递动态内存,Test1函数中的
//str⼀直都是NULL。strcpy(str, "hello world"),将使程序奔溃
void Test1(void)
{char* str = NULL;GetMemory1(str);strcpy(str, "hello world");printf(str);
}char *GetMemory2(void)
{char p[] = "hello world";return p;
}//可能是乱码。 因为GetMemory2返回的是指向“栈内存”的指针,该指针的地址不是NULL,
//使其原现的内容已经被清除,新内容不可知。
void Test2(void)
{char *str = NULL;str = GetMemory2();printf(str);
}void GetMemory3(char** p, int num) {*p = (char*)malloc(num);
}//能够输出hello, 内存泄露。GetMemory3申请的内存没有释放
void Test3(void)
{char* str = NULL;GetMemory3(&str, 100);strcpy(str, "hello");printf(str);
}//篡改动态内存区的内容,后果难以预料。⾮常危险。
//因为 free(str);之后,str成为ᰀ指针,if(str != NULL)语句不起作⽤。
void Test4(void)
{char *str = (char*)malloc(100);strcpy(str, "hello");free(str);if(str != NULL) {strcpy(str, "world");cout << str << endl;}
}

3. 实现内存拷贝函数

char* strcpy(char *dst, const char *src) //[1]
{assert(dst != NULL && src != NULL);  //[2]char *ret = dst;                     //[3]while((*dst++ = *src++) != '\0';     //[4]return ret;
}

[1] const修饰:

源字符串参数用const修饰,防止修改源字符串。

[2] 空指针检查:

(1)不检查指针的有效性,说明不注重代码的健壮性。

(2)检查有效性时使用assert(!dst && !src); char*转换为bool即是类型隐式转换,这种功能虽然灵活,但是更多的是导致出错率增大和维护成本提高。

(3)检查指针有效性时使用assert(dst != 0 && src != 0); 直接使用常量会减少程序的可维护性。而使用NULL代替0,如果出现拼写错误,编译器会检查出来。

[3] 返回目标地址

忘记保存原来的strdst值。

[4]'\0'

(1)循环写成while(*dst++=*src++);

(2)循环写成while (*src!='\0') *dst++ = *src++; 循环结束后没有正确加上'\0'

(3)返回dst的原始值使函数能够支持链式表达式

4. 假如考虑dst和src内存重叠的情况,strcpy该怎么实现

char s[10]="hello";strcpy(s, s+1);
// 应返回 ellostrcpy(s+1, s);
// 应返回 hhello 但实际会报错
// 因为dst与src᯿叠了,把'\0'覆盖了

所谓重叠,就是src未处理的部分已经被dst给覆盖了,只有一种情况:src<=dst <= src+strlen(src)

C函数memcpy自带内存重叠检测功能。

char * strcpy(char *dst,const char *src) {assert(dst != NULL && src != NULL);char *ret = dst;my_memcpy(dst, src, strlen(src)+1);return ret; }
/* my_memcpy的实现如下 */
char *my_memcpy(char *dst, const char* src, int cnt) {assert(dst != NULL && src != NULL);char *ret = dst;/*内存᯿叠,从⾼地址开始复制*/if (dst >= src && dst <= src+cnt-1){dst = dst+cnt-1;src = src+cnt-1;while (cnt--){*dst-- = *src--;}}else //正常情况,从低地址开始复制{while (cnt--){*dst++ = *src++;}}return ret; }

5. 按照下面要求写程序

已知string的原型为:

class String
{
public:String(const char *str = NULL);String(const String &other);~ String(void);String & operate =(const String &other);
private:char *m_data;
};
// 构造函数
String::String(const char *str) {if(str==NULL){m_data = new char[1]; //对空字符串⾃动申请存放结束标志'\0'*m_data = '\0';} else{int length = strlen(str);m_data = new char[length + 1];strcpy(m_data, str);}
}// 析构函数
String::~String(void) {delete [] m_data; // 或delete m_data;
}
//拷⻉构造函数
String::String(const String &other) {int length = strlen(other.m_data);m_data = new char[length + 1];strcpy(m_data, other.m_data);
}
//赋值函数
String &String::operate =(const String &other)
{ if(this == &other){return *this; // 检查⾃赋值} delete []m_data; // 释放原有的内存资源int length = strlen(other.m_data);m_data = new char[length + 1]; //对m_data加NULL判断strcpy(m_data, other.m_data); return *this; //返回本对象的引⽤
}

6. 说一说进程的地址空间分布

对于一个进程,其空间分布如下:

如上图,从高地址到低地址,一个程序由命令行参数和环境变量、栈、文件映射区、堆、BSS段、数据段、代码段组成。

(1)命令行参数和环境变量

命令行参数是指从命令行执行程序的时候,给程序的参数。

(2)栈区

存储局部变量、函数参数值。栈从高地址向低地址增长。是一块连续的空间。

(3)文件映射区

位于堆和栈之间。

(4)堆区

动态申请内存用。堆从低地址向高地址增长。

(5)BSS段

存放程序中未初始化的全局变量和静态变量的一块内存区域。

(6)数据段

存放程序中已初始化的全局变量和静态变量的一块内存区域。

(7)代码段

存放程序执行代码的一块内存区域。只读,代码段的头部还会包含一些只读的常数变量。

7. 说一说C与C++的内存分布方式

(1)从静态存储区域分配

内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在,如全局变量,static变量。

(2)在栈上创建

在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放,栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

(3)从堆上分配(动态内存分配)

程序在运行的时候用malloc或new申请任意多少的内存,程序员负责在何时用free或delete释放内存。动态内存的生存周期由自己决定,使用非常灵活。

8. new、delete、malloc、free关系

如果是带有自定义析构函数的类类型,以如下代码为例,

class A {};
A* pAa = new A[3];
delete pAa;

用 new[] 来创建类对象数组,而用delete来释放的话,会:
        (1)调用一次指向的对象的析构函数

(2)调用operator delete()释放内存

显然,这是对数组的第一个类对象调用了析构函数,后面的两个

个对象均没调⽤析构函数,如果类对象中申请了大量的内存需要在析构函数中释放,而你却在销毁数组对象时少调用了析构函数,这会造成内存泄漏。
上⾯的问题你如果说没关系的话,那么第⼆点就是致命的了!直接释放pAa指向的内存空间,这个总是会造成严重的 段错误,程序必然会奔溃!因为分配的空间的起始地址是 pAa 指向的地⽅减去 4 个字节的地方。你应该传⼊参数设为那个地址!

计算机中的乱序执行

1. 一定会按正常顺序执行的情况

1. 对同一块内存进行访问,此时访问的顺序不会被编译器修改

2. 新定义的变量的值依赖于之前定义的变量,此时两个变量定义的顺序不会被编译器修改

2. 其他情况计算机会进行乱序执行

单线程的情况下允许,但是多线程情况下会产生问题

3. C++中的库提供了六种内存模型

用于在多线程的情况下防止编译器的乱序执行

(1)memory_oreder_relaxed

最放松的

(2)memory_oreder_consume

当客户使用,搭配release使用,被release进行赋值的变量y,获取的时候如果写成consume,那么所有与y相关的变量的赋值一定会被按顺序进行

(3)memory_oreder_aquire

用于获取资源

(4)memory_oreder_release

一般用于生产者,当给一个变量y进行赋值的时候,只有自己将这个变量释放了,别人才可以去读,读的时候如果使用acquire来读,编译器会保证在y之前被赋值的变量的赋值都在y之前被执行,相当于设置了内存屏障。

(5)memory_oreder_acq_rel(acquire/release)

(6)memory_oreder_seq_cst(squentially consistent)

好处:不需要编译器设置内存屏障,morden C++开始就会有底层汇编的能力

副作用

1. 无副作用编程

存在一个函数,传一个参数x进去,里面进行一系列的运算,返回一个y。中间的所有过程都是在栈中进行修改

2. 有副作用编程

比如在一个函数运行的过程中对全局变量进行了修改或在屏幕上输出了一些东西。此函数还有可能是类的成员方法,在此方法中如果对成员变量进行了修改,类的状态就会发生改变

3. 在多线程情况下有副作用的编程

在线程1运行的时候对成员变量进行了修改,此时如果再继续运行线程2,此时线程2拥有的就不是这个类的初始状态,运行出来的结果会受到线程1的影响

解决办法:将成员方法设为const

信号量

1.binary_semaphore

定义:
可以当事件来用,只有有信号和无信号两种状态,一次只能被一个线程所持有。

使用步骤:

(1)初始创建信号量,并且一开始将其设置为无信号状态

(2)线程使用acquire()方法等待被唤醒

(3)主线程中使用release()方法,将信号量变成有信号状态

2. counting_semaphore

定义:

一次可以被很多线程所持有,线程的数量由自己指定

使用步骤:

(1)创建信号量

指定一次可以进入的线程的最大数量,并在最开始将其置位称为无信号状态:std::biinary_semaphore<8> sem(0);

(2)主线程中创建10个线程

并且这些线程全都调用acquire()方法等待被唤醒。但是主线程使用realease(6)方法就只能随即启动六个线程。

future库

用于任务链(即任务A的执行必须依赖于任务B的返回值)

1. 例子:生产者消费者模型

(1)⼦线程作为消费者,参数是⼀个future,⽤这个future等待⼀个int型的产品:std::future& fut
(2)⼦线程中使⽤get()⽅法等待⼀个未来的future,返回⼀个result
(3)主线程作为⽣产者,做出⼀个承诺:std::promise prom
(4)⽤此承诺中的get_future()⽅法获取⼀个future
(5)主线程中将⼦线程创建出来,并将刚刚获取到的future作为参数传⼊
(6)主线程做⼀些列的⽣产⼯作,最后⽣产完后使⽤承诺中的set_value()⽅法,参数为刚刚⽣产出的产品
(7)此时产品就会被传到⼦线程中,⼦线程就可以使⽤此产品做⼀系列动作
(8)最后使⽤join()⽅法等待⼦线程停⽌,但是join只适⽤于等待没有返回值的线程的情况
2. 如果线程有返回值
(1)使⽤async⽅法可以进⾏异步执⾏
参数⼀: 可以选择是⻢上执⾏还是等⼀会执⾏(即当消费者线程调⽤get()⽅法时才开始执⾏)
参数⼆: 执⾏的内容(可以放⼀个函数对象或lambda表达式)
(2)⽣产者使⽤async⽅法做⽣产⼯作并返回⼀个future
(3)消费者使⽤future中的get()⽅法可以获取产品

运算符重载

重载运算符函数,本质还是函数调用,所以重载后:

(1)可以是和调用运算符的方式调用,data1 + data2

(2)也可以是调用函数的方式,operator+(data1, data2),这就要注意运算符函数的名字是“operator运算符”

在可以重载的运算符里有逗号,取地址,逻辑与,逻辑或

不建议重载:

逗号,取地址,本身就是对类类型有特殊意义;逻辑与、逻辑或,有短路求值属性;逗号、逻辑与、或,定义了求职顺序。

运算符重载应该是作为类的成员函数or非成员函数。

注意:
重载运算符,本身是几元就有几个参数,对于二元的,第一个参数对应左侧运算对象,第二个参数对应右侧运算对象。而!类的成员函数的第一个参数隐式的绑定this指针,所以重载运算符如果是类的成员函数,左侧运算对象就相当于固定了是this。

建议非成员:

又因为要访问类的私有成员,多为类的友元。返回值iostream的引用,第一个参数iostream的引用,第二个参数,输出用const,输入非常量。输入的重载里注意判断是否成功,避免输入了不合预期的内容。

一些规则:

(1)算术和关系运算符建议非成员

这些运算符是对称性的,形参都是常量引用

(2)赋值运算符必须成员。复合运算符建议成员

(3)下标运算符必须成员

返回访问元素的引用,建议两版本(常量、非常量)

(4)递增递减运算符,建议成员

因其会改变对象状态,后置与前置的区分——接受一个额外的不被使用int类型形参,前置返回变后的对象引用,后置返回对象的原值(非引用);解引用(*)建议成员,因其与给定类型关系密切,箭头(->)必须成员。

函数调用运算符:

lambda是函数对象。编译器是将lambda表达式翻译为一个未命名类的未命名对象,'['捕获列表']'(参数列表){函数体} 对应类中重载调用运算符的参数列表、函数体,捕获列表的内容就对应类中的数据成员。所以捕获列表、值传递时,要拷贝并初始化那些数据成员,引用传递就是直接用

[C++ STL]

STL实现原理机器实现

STL提供了六大组件,彼此之前可以组合套用,这六大组件分别是:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器。

STL六大组件的交互关系:

1. 容器通过空间配置器取得数据存储空间

2. 算法通过迭代器存储容器中的内容

3. 仿函数可以协助算法完成不同的策略的变化

4. 适配器可以修饰仿函数

容器

各种数据结构,如vector、list、deque、set、map等,用来存放数据,从实现角度来看,STL是一种class template。

算法

各种常用的算法,如sort,find,copy,for_each。从实现角度来看,STL算法是一种function template。

迭代器

扮演了容器与算法之间的胶合剂,共有五种类型,从实现角度来看,迭代器是一种将operator->,operator*,operator++,operator--等指针相关操作予以重载的class template。

所有STL容器都附带有自己专属的迭代器,只有容器的设计者才知道如何遍历自己的元素。

原生指针(native pointer)也是一种迭代器。

仿函数

行为类似函数,可作为算法的某种策略。从实现角度来看,仿函数是一种重载了operator()的class或者class template。

适配器

一种用来修饰容器或者仿函数或迭代器接口的东西。

STL提供的queue和stack,虽然看似容器,但其实只能算是一种容器配接器,它们的底部完全借助deque,所有操作都由底层deque供应。

空间配置器

负责空间的配置和管理,从实现角度看,配置器是一个实现了动态空间配置、空间管理、空间释放的class template。

一般的分配器的std::alloctor都含有两个函数allocate 和 deallocate,这两个函数分别调用operator new() 与 delete(),这两个函数底层又分别是malloc() 和 free();但是每次malloc会带来额外的开销(每次malloc一个元素都要带有附加信息)

容器之间的实现关系及分类:

STL的优点

STL 具有⾼可复⽤性,⾼性能,⾼移植性,跨平台的优点。
1、⾼可重⽤性:
STL 中⼏乎所有的代码都采⽤了模板类和模版函数的⽅式实现,这相⽐于传统的由函数和类组成的库来说提供了更好的代码复⽤机会。
2、⾼性能:
如 map 可以⾼效地从⼗万条记录⾥⾯查找出指定的记录,因为 map 是采⽤红⿊树的变体实现的。
3、⾼移植性:
如在项⽬ A 上⽤ STL 编写的模块,可以直接移植到项⽬ B 上。
STL 的⼀个重要特性是将数据和操作分离
数据由容器类别加以管理,操作则由可定制的算法定义。迭代器在两者之间充当“粘合剂”,以使算法可以和容器交互运作。

pair容器

保存两个数据成员,用来生成特定类型的模板。
使用:pair<T1, T2>p;
数据成员是public,两个成员分别是 first 和 second
其中map的元素是pair, pair<const key_type, mapped_type>
可以用来遍历关联容器
map<string, int>p;
auto map1 = p.cbegin();
while(map1 != p.cend())
{cout<<map1->first<<map1->second<<endl;++map1;
}

对map进行插入,元素类型是pair:

p.insert({word, 1});
p.insert(pair<string, int>(word, 1));

insert对不包含重复关键字的容器,插入成功返回pair<迭代器, bool>迭代器指向给定关键字元素,bool指出插入是否成功。

vector<pair<char, int>result(val.begin(), val.end());
sort(result.begin(), result.end(), [](auto &a, auto &b){return a.send > b.second;
})

vector容器实现与扩充

1. 底层实现

vector在堆中分配了一段连续的内存空间来存放元素。

1. 三个迭代器

(1)first:指向的是vector中对象的起始字节位置

(2)last:指向当前最后一个元素的末尾字节

(3)end:指向整个vector容器所占用内存空间的末尾字节

(1)last - first:表示vector容器中目前已被使用的内存空间

(2)end - first:表示vector容器的容量

(3)end - last:表示vector容器目前空闲的内存空间

2. 扩容过程

如果集合已满,在新增数据的时候,就要分配一块更大的内存,将原来的数据复制过来,释放之前的内存,再插入新增的元素

所以对vector的任何操作,一旦引起空间重新配置,指向原来vector的所有迭代器就都失效了

size() 和 acpacity()

(1)堆中分配内存,元素连续存放,内存空间只会增长不会减少

vector有两个函数,一个是capacity(),在不分配新内存下最多可以保存的元素个数,另一个size(),返回当前已经存储数据的个数

(2)对于vector来说,capacity是永远大于size的

capacity和size相等时,vector就会扩容,capacity变大(翻倍)

vector扩容方式的选择

1. 固定扩容

机制:

每次扩容的时候在原有capacity基础上加上固定的容量。

缺点:

考虑一种极端情况,vector每次添加的元素数量刚好等于每次扩容固定增加的容器+1,就会造成一种情况,每添加一次元素就要扩容一次,而扩容的时间花费十分昂贵,时间复杂度高

优点:

空间利用率比较高。

2. 加倍扩容

机制:

每次扩容的时候原capacity翻倍。

优点:

一次扩容capacity翻倍的方式使得正常情况下添加元素要扩容的次数大大减小(预留空间较多),时间复杂度低。

缺点:

空间利用率低

3. resize() 和 reserve()

resize():改变当前容器内含有元素的数量,而不是容器的容量

1. 当resize(len)中len > v.capacity(),则数组中的size和capacity均设置为len;

2. 当resize(len)中len <= v.capacity(),则数组中的size设置为len,capacity不变

reserve():改变当前容器的最大容量(capacity)

1. 如果reserve(len)的值 > 当前的capacity(),那么会重新分配一块能存len个对象的空间,然后把之前的对象通过copy constructor复制过来,销毁之前的内存;

2. 当reserve(len)中len的值<=当前的capacity(),则不对容器做任何改变。

3. vector源码

template<class T, class Alloc = alloc>
class vector {
public:typedef T value_type;typedef value_type* pointer;typedef value_type& reference;typedef size_t size_type;typedef ptrdiff_t difference_type;//嵌套类型定义,也可以是关联类型定义protected:typedef simple_allloc <value_type, Alloc> data_alloc//空间配置器(分配器)iterator start;iterator finish;iterator end_of_storage;//这三个就是vector里的数据,所以一个vector就是包含三个指针12bytevoid insert_aux(iterator position, const T& x);//这个就是vector自动扩充函数void deallocate() {if (start)data_allocator::deallocate(static_assert, end_of_storage);}//析构函数的部分实现函数void fill_initialize(size_type n, const T& value) {start = allocate_end_fill(n, value);finish = start + n;end_of_storage = finish;}//构造函数的具体实现public:iterator begin() { return start; };iterator end() { return finish; };size_type size() const { return size_type(end() - begin()); };size_type capacity() const {return size_type(end_of_storage - begin()) }bool empty() const { return begin() == end(); };reference operator[](size_type n) { return *(begin() + n); };//重载[]说明vector支持随即访问vector()::start(0), finish(0), end_of_storage(0) {};vector(size_type n, const T& value)(fill_initialize(n, value););vector(long n, const T& value)(fill_initialize(n, value););vector(int n, const T& value)(fill_initialize(n, value););explicit vector(size_type n) { fill_initialize(n, T()); };//重载析构函数~vector() {destroy(start, finish);//全局函数,析构对象deallocate(); //成员函数,释放空间 }//功能函数reference front() { return *begin(); };reference back() { return *(end() - 1); };void push_back(const T& x) {if (finish != end_of_storage) {construct(finish, x);++finish;}else insert_aux(end(), x);//先扩充再添加}void pop_back() {destroy(finish);--finish;}iterator erase(iterator position) {if (position + 1 != end())copy(position + 1, finish, position);--finish;destroy(finish);return position;}void resize(size_type new_size, const T& x) {if (new_size() < size())erase(begin() + new_size, end());elseinsert_aux(end(), new_size - size, x);}void resize()(size_type new_size) {resize(new_size, T()); }void clear() { erase(begin(), end()); }protected://配置空间并填满内容iterator allocate_and_fill(size_type n, const T& x) {iterator rsult = data_allocator::allocate(n);uninitialized_fill_n(result, n, x); //全局函数}
};

vector迭代器:由于vector维护的是一个线性区间,所以普通指针具备作为vector迭代器的所有条件,就不需要重载operator+, operator*之类的东西。

//vector迭代器
template <class T, class Alloc = alloc>
class vector {
pbulic:typedef T value_type;typedef value_type* iterator; //vector的迭代器是原生指针//...
};

vector的数据结构:线性空间。为了降低配置空间成本,我们必须让其容量大于其大小。

vector的构造以及内存管理:当我们使用push_back插入元素在尾端的时候,我们首先检查是否还有备用空间也就是说end是否等于end_of_storage,如果有直接插入,如果没有就扩充空间

//vector的构造以及内存管理
template<class T, class Alloc>
void vector<T, Alloc>::insert_aux(iterator position, const T& x) {if (finish != end_of_storage) { //有备用空间construct(finish, *(finish - 1)); //在备用空间处构造一个元素,以vector最后一个元素为初值++finish;T x_copy = x;copy_backward(position, finish - 2, finish - 1);*position = x_copy;}else {const size_type old_size = size();const size_type len = old_size != 0 ? 2 * old_size() : 1;//vector中没有元素就配置一个元素,如果有救配置两倍元素iterator new_start = data_allocator::allocate(len);iterator new_finish = new_start;try {//拷贝插入点之前的元素new_finish = uninitialized_copy(start, position, new_start);construct(new_finish, x);++new_finish;//拷贝插入点之后的元素new_finish = uninitialized_copy(position, finish, new_finish);}catch () {destroy(new_satrt, new_finish);data_allocator::deallocate(new_start, len);throw;}//析构并释放原vectordestroy(begin(), end());deallocate();//调整迭代器指向新的vectorstart = new_start;finish = new_finish;end_of_storage = new_start + len;}
}

整个分为三个部分,配置新空间,转移元素,释放原来的元素与空间,因此一旦引起空间配置指向之前vector的所有迭代器都要失效。

//vector的部分元素操作
void pop_back() {--finish;destroy(finish);
}//erase版本一:范围清楚元素iterator erase(iterator first, iterator last) {iterator i = copy(last, finish, first);destroy(i, finish);finish = finish - (last - first);return first;
}iterator erase(iterator position) {if (position + 1 != finish) {copy(position + 1, finish, position);--finish;destroy(finish);return position;}
}void clear() { erase(begin(), end()) };
template<class T, class Alloc>
void vector<T, Alloc>::insert(iterator position, size_type n, const T& x) {if (n != 0) {if (size_type(end_of_storage - finish) > n) {//备用空间大于插入的元素数量T x_copy = x;//以下计算插入点之后的现有元素个数const size_type elems_after = finish - position;iterator old_finish = finish;if (elems_after > n) {//插入点之后的元素个数大于要插入的元素个数uninitialiazed_copy(finish - n, finish, finish);finish += n; //将vetor的尾端标记后移copy_backward(position, old_finish - n, old_finish);fill(position, old_finish, x_copy); //从插入点之后开始插入新值}else {//插入点之后的元素个数小于要插入的元素个数uninitaliazed_fill_n(finish, n - elems_after, finish);finish += n - elems_after;uninitialiazed_copy(position, old_finish, finish);finish += elems_after;fill(position, old_finish, x_copy);}else {//备用空间小于要插入元素的个数//首先决定新长度,原长度的两倍,或者老长度+新的元素个数const size_type old_size = size();const size_type len = old_size + max(old_size, n);//以下配置的新空间iterator new_start = data_allocator::allocate(len);iterator new_finish = new_start;_STL_TRY{//拷贝插入点之前的元素new_finish = uninitialized_copy(static_assert, position, new_start);//把新增元素(初值皆为n)传入新空间new_finish = uninitialized_fill_n;//拷贝插入点之后的元素new_finish = uninitialized_copy(position, finish, new_finish);//有利于理解上面的insert_aux函数}#ifdef _STL_USE_EXCEPTIONScatch () {//如果有异常发生destroy(new_start, new_finish);data_allocator::deallocate(new_start, len);throw;}
#endif /* _STL_USE_EXCEPTION*///析构并释放原vectordestroy(begin(), end());deallocate();//调整迭代器指向新的vectorstart = new_start;finish = new_finish;end_of_storage = new_start + len;}}}
}

list-链表

list设计

每个元素都是放在一块内存中,它的内存空间可以是不连续的,通过指针来进行数据的访问

在哪里添加删除元素都很高,不需要移动内存,当然也不需要对每个元素都进行构造和析构,所以通常用来做随机插入和删除操作的容器。

list属于双向链表其节点和list本身是分开设计的:

template<class T, class Alloc = alloc>
class list {
protected:typedef listnode <T> listnode;
public:typedef listnode link_type;typedef listiterator<T, T&, T> iterator;
protected:link_type node;
};

学习到了一个分析方法,拿到这样一个类,先看它的数据比如上面的link_type node,然后我们再看它的前缀,link_type,去上面在link_type,找到typedef listnode link_type;按这个方法继续找到上面的type listnode <T> listnode;我们发现list_node 是下面的类,我们一层层的寻找,就能看懂。

template<class T>
struct _listnode {typedef void voidpointer;void_pointer prev;void_pointer next;T data;
};

list是一个环状的双向链表,同时它也满足STL对于“前闭后开”的原则,即在链表尾端可以加上空白节点。

list的迭代器的设计:

迭代器是泛化的指针所以里面重载了->, --, ++, * () ,等运算符,同时迭代器是算法与容器之间的桥梁,算法需要了解容器的方方面面,于是就诞生了5种关联类型,(这5种类型是必备的,可能还需要其他类型)我们知道算法传入的是迭代器或者指针,算法根据传入的迭代器或者指针推断出算法所想要了解的容器里的5种关联类型的相关信息。由于光传入指针,算法推断不出来想要的信息,所以我们需要一个中间商(萃取器)也及时我们所说的iterator traitis类,对于一般的迭代器,它直接提供迭代器里的关联类型值,而对于指针和常量指针,它采用的类模板偏特化,从而提供其所需要的关联类型的值。

//针对一般的迭代器类型,直接取迭代器内定义的关联类型
template<class I>
struct iterator_traits {typedef typename I::iteratorcategory iteratorcategory;typedef typename I::valuetype valuetype;typedef typename I::differencetype differencetype;typedef typename I::pointer pointer;typedef typename I::reference reference;
};//针对指针类型进行特化,指定关联类型的值
template<class T>
struct iteratortraits<T> {typedef randomaccessiteratortag iteratorcategory;typedef T value_type;typedef ptrdifft differencetype;typedef T* pointer;typedef T& reference;
};//针对指针常量类型进行特化,指定关联类型的值
template<class T>
struct iteratortraits<const T> {typedef randomaccessiteratortag iteratorcategory;typedef T valuetype; //valuetype被用于创建变量,为灵活起见,取T而非const T作为value_typetypedef ptedifft differencetype;typedef const T* pointer;typedef const T& reference;
};

vector 和 list 的区别

1. vector底层实现是数组;list是双向链表

2. vector是顺序内存,支持随机访问,list不行

3. vector在中间节点进行插入删除会导致内存拷贝,list不会

4. vector一次性分配好内存,不够时才进行翻倍扩容;list每次插入新节点都会进行内存申请

5. vector随机访问性能好,插入删除性能差;list随机访问性能差,插入删除性能好

deque-双端数组

支持快速随机访问,由于deque需要处理内部跳转,因此速度上没有vector快。

1. deuqe概述

deque是一个是一个双端开口的连续线性空间,其内部为分段连续的空间组成,随时可以增加一段新的空间并链接。

注意:

由于deque的迭代器比vector要复杂,影响了各个运算层面,所以非必要尽量使用vector;为了提供效率,在对deque进行排序操作的时候,我们可以先把deque复制到vector中再进行排序最后再复制回deque

2. deque中控器

deque是由一段一段的连续空间构成。一旦有必要在其头端或者尾端增加新的空间,便配置一段定量连续空间,串接在整个deque的头端或者尾端

好处:

避免“vector的重新配置,复制,释放”的轮回,维护链整体连续的假象,并提供随机访问接口;

坏处:

其迭代器变得很复杂

deque采用一块map作为主控,其中的每个元素都是指针,指向另一片连续线性空间,称之为缓存区,这个区才是存储数据的。

template<class T, class Alloc = alloc, size_t Bufsize = 0>
class deque {public:typedef T value_type;typedef value_type pointer*;typedef size_t size_type;//...public:typedef _deque_iterator<T, T&, T*, Bufzie> iterator;protected:typedef pointer* map_pointer;protected:iterator start;iterator finish;map_pointer map; //指向mapsize_type map_size; //map内可容纳多少指针
};//map其实是一个T**

deque迭代器:

//迭代器
//迭代器的关键⾏为,其中要注意的是⼀旦遇到缓冲区边缘,可能需要跳⼀个缓存区
void set_node(map_pointer new_node) {node = new_node;first = *new_node;last = first + difference_type(buffer_size());
}
//接下来᯿重载运算⼦是_deque_iterator<>成功运作的关键
reference operator*() const { return *cur; }pointer operator->() const { return &(operator*()); }difference_type operator-(const self& x) const {return difference_type(buffer_szie()) * (node - x.node - 1) + (cur - first) + (x.last - x.cur);
}
self& operator++() {++cur;if (cur == last) {set_node(node + 1);cur = first;}return *this;
}
self operator++(int) {self temp = *this;++* this;return temp;
}
self& operator--() {if (cur == first) {set_node(node - 1);cur = last;}--cur;return *this;
}
self operator-(int) {self temp = *this;--* this;return temp;
}
//以下实现随机存取,迭代器可以直接跳跃n个距离
self& operator+=(difference_type n) {difference_type offest = n + (cur - first);if (offest > 0 && offest < difference_type(buffer_size()))cur += n;else {offest > 0 ? offest / fifference_type(buffer_size()) : -difference_type((-offest - 1) / buffer_size()) - 1;set_node(node + node_offest);cur = first + (offest - node_offest * difference_type(buffer_size()));}return *this;
}
self operator+(differnece_type n) {self tmp = *this;return tmp += n;
}
self operator-=() { return *this += -n; }
self operator-(difference_type n) {self temp = *this;return *this -= n;
}
rference operator[](difference_type n) {return *(*this + n);
}
bool operator==(const self& x) const { return cur == x.cur; }
bool operator!=(const self& x) const { return !(*this == x); }
bool operatoe < (const self& x) const {return (node == x.node) ? (cur < x.cur) : (node - x.node);
}

deque拥有两个数据成员

start与finish迭代器,分别由deque:begin()与deque:end()传回

//迭代器的关键⾏为,其中要注意的是⼀旦遇到缓冲区边缘,可能需要跳⼀个缓存区
void set_node(map_pointer new_node) {node = new_node;first = *new_node;last = first + difference_type(buffer_size());
}
//接下来重᯿载运算⼦是_deque_iterator<>成功运作的关键
reference operator*() const { return *cur; }
pointer operator->() const { return &(operator*()); }
difference_type operator—(const self& x) const {return difference_type(buffer_szie()) * (node - x.node - 1) + (cur - first) + (x.last - x.cur);
}
self& operator++() {++cur;if (cur == last) {set_node(node + 1);cur = first;}return *this;
}
self operator++(int) {self temp = *this;++* this;return temp;
}
self& operator--() {if (cur == first) {set_node(node - 1);cur = last;}--cur;return *this;
}
self operator-(int) {self temp = *this;--* this;return temp;
}
//以下实现随机存取,迭代器可以直接跳跃n个距离
self& operator+=(difference_type n) {difference_type offest = n + (cur - first);if (offest > 0 && offest < difference_type(buffer_size()))cur += n;else {offest > 0 ? offest / fifference_type(buffer_size()) : -difference_type((-offest - 1) / buffer_size()) - 1;set_node(node + node_offest);cur = first + (offest - node_offest * difference_type(buffer_size()));}return *this;
}
self operator+(differnece_type n) {self tmp = *this;return tmp += n;
}
self operator-=() { return *this += -n; }
self operator-(difference_type n) {self temp = *this;return *this -= n;
}
rference operator[](difference_type n) {return *(*this + n);
}
bool operator==(const self& x) const { return cur == x.cur; }
bool operator!=(const self& x) const { return !(*this == x); }
bool operatoe < (const self& x) const {return (node == x.node) ? (cur < x.cur) : (node - x.node);
}

deque数据结构:

deque除了维护一个map指针以外,还维护了start与finish迭代器分别指向第一缓冲区的第一个元素,和最后一个缓冲区的最后一个元素的下一个元素,同时它还必须记住当前map的大小。具体结构和源代码见上。

deque的构造与管理

//deque首先自行定义了两个空间配置器
typedef simple_alloc <value_type, Alloc> data_allocator;
typedef simple_alloc <pointer, Alloc> map_allocator;

deque中有一个构造函数用于构造deque结构并赋初值

deque(int n, const value_type& value) : start(), finish(), map(0), map_size(0) {fill_initialize(n, value);//这个函数就是⽤来构建deque结构,并设⽴初值
}
template<class T, class Alloc, size_t BufSize)void deque<T, Alloc, BufSize>::fill_initialize(size_type n, const value_type& value) {creat_map_and_node(n);//安排结构map_pointer cur;_STL_TRY{//为每个缓存区赋值for (cur = start.node; cur < finish.node; ++cur)uninitalized_ fill(*cur, *cur + buffer_size(), value);//设置最后⼀个节点有⼀点不同uninitalized_fill(finish.first, finish.cur, value);}catch () {// ...}
}
template<class T, class Alloc, size_t Bufsize>
void deque<T, alloc, Bufsize>::creat_map_and_node(size_type num_elements) {//需要节点数=元素个数/每个缓存区的可容纳元素个数+1size_type num_nodes = num_elements / Buf_size() + 1;map_size = max(initial_map_size(), num_nodes + 2);//前后预留2个供扩充//创建⼀个⼤⼩为map_size的mapmap = map_allocator::allocate(map_size);//创建两个指针指向map所拥有的全部节点的最中间区段map_pointer nstart = map + (map_size() - num_nodes) / 2;map_poniter nfinish = nstart + num_nodes - 1;map_pointer cur;_STL_TRY{//为每个节点配置缓存区for (cur = nstart; cur < nfinish; ++cur)+ cur = allocate_node();}catch () {// ...}//最后为deque内的start和finish设定内容start.set_node(nstart);finish.set_node(nfinish);start.cur = start.first;finish.cur = finish.first + num_elements % buffer_szie();
}

接下来就是插入操作的实现。第一,首先判断是否有扩充map的需求,若有就扩充,然后就是在插入函数中,首先判断是否在结尾或开头从而判断是否跳跃节点。

// 由于尾端只剩⼀个可⽤元素空间(finish.cur=finish.last-1),
// 所以我们必须᯿新配置⼀个缓存区,在设置新元素的内容,然后更改迭代器的状态
tempalate<class T, class Alloc, size_t BufSize>
void deque<T, alloc, BufSize>::push_back_aux(const value_type& t) {value_type t_copy = t;reserve_map_at_back();*(finish.node + 1) = allocate_node();_STL_TRY{construct(finish.cur, t_copy);finish.set_node(finish.node + 1);finish.cur = finish.first;}- STL_UNWIND{deallocate_node(*(finish.node + 1));}
}
//push_front也是⼀样的逻辑
deque vector
组织方式 按页或块来分配存储的,每页包含固定数目的元素 分配一段连续的内存来存储内容
效率 即使在容器的前端也可以提供常数时间的insert和erase操作,而且体积增长方面也更有效率 只在序列尾端插入时才有效率,但是随即访问速度更快

stack 和 queue

概述:栈与队列被称之为deque的配接器,其底层实现是以deque为底部架构。通过deque执行具体操作。

源码

template<class T, class Sequence = deque <T>>class stack { //_STL_NULLL_TMPL_ARGS展开为<>friend bool operator ==_STL_NULL_TMPL_ARGS(const stack&, const stack&);friend bool operator<_STL_NULL_TMPL_ARGS(const stack&, const stack&);public:typedef typename Sequence::value_type value_type;typedef typename Sequence::size_type size_type;typedef typename Sequence::reference reference;typedef typename Sequence::const_reference const_refernece;
protected:Sequence c;public:bool empty() const { return c.empty(); }size_type size() const { return c.size(); }reference top() {return c.back();}const_rference top() const { return c.back(); }void push(const value_type& x) { c.push_back(x); }void pop_back() { c.pop_back(); }
};template<class T, class Sequence>
bool operator==(const stack<T, Sequence>& x, const stack<T, Sequence>& y) {return x.c == y.c;
}template<class T, class Sequence>
bool operator<(const stack<T, Sequence>& x, const stack<T, Sequence>& y) {return x.c < y.c;
}
};

heap and priority_queue

heap(堆):

建立在完全二叉树上,分为两种,大根堆,小根堆,其在STL中做priority_queue的助手,即,以任何顺序将元素堆入容器中,然后取出时一定是从优先权最高的元素开始取,完全二叉树具有这样的性质,适合做priority_queue的底层。

priority_queue:

优先队列,也是配接器。其内的元素不是按照被堆入的序列排序,而是自动取元素的权值排列,缺省情况下利用一个max-heap完成,后者是以vector表现的完全二叉树

template<class T, class Sequence=vector <T>, class Compare = less<typename Sequence::value_type>>
class priority_queue {public:typedef typename Sequence::value_type value_type;typedef typename Sequence::size_type size_type;typedef typename Sequence::reference reference;typedef typename Sequence::const_reference const_refernece;protected:Sequence c;//底层容器Compare comp;//容器⽐较⼤⼩标准public:priority_queue() : c() {}explicit priority_queue(const Compare& x) : c(), comp(x) {}//以下⽤到的make_heap(),push_heap(),pop_heap()都是泛型算法//任何⼀个构造函数都可以⽴即在底层产⽣⼀个heaptemplate<class InputIterator>priority_queue(InputIterator first, InputIterator last const Compare& x): c(first, last), comp(x) {make_heap(c.begin(), c.end(), comp);}template<class InputIterator>priority_queue(InputIterator first, InputIterator last const Compare& x): c(first, last) {make_heap(c.begin(), c.end(), comp);}bool empty() const { return c.empty(); }size_type size() const { return c.size(); }const_reference top() const { return c.front(); }void push(const value_type& x) {_STL_TRy{c.push_back(X);push_heap(c.begin(), c.end(), comp);}_STL_UNWIND{ c.clear() };}void pop() {_STL_TRY{pop_heap(c.begin(), c.end(), comp);c.pop_back();}_STL_UNWEIND{ c.clear() };}
};
// priority_queue⽆迭代器
};

map && set

共同点:

都是C++的关联容器,只是通过它提供的接口对里面的元素进行访问,底层实现都是红黑树。

不同点:

set:用来判断某一个元素是不是在一个组里面。

map:映射,相当于字典,把一个值映射到另一个值,可以创建字典。

优点:

查找某一个数的时间为Olog(n);遍历时采用iterator,效果不错。

缺点:

每次插入值的时候,都需要调用红黑树,效率有一定影响。

3. 细节

1. 为什么要成倍的扩容而不是一次增加一个固定大小的容器?

采用成倍方式扩容,可以保证常数的时间复杂度,而增加指定大小的容器只能达到O(n)的时间复杂度。

2. 为什么以两倍的方式扩容而不是三倍四倍,或者其他方式呢?

考虑可能产生的堆空间浪费,所以增长倍数不能太大,一般是1.5或2;GCC是2;VS是1.5,k = 2每次扩展的新尺寸必然刚好大于之前分配的总和,之前分配的内存空间不可能被使用,这样对于缓冲并不友好,采用1.5倍的增长方式可以更好实现对内存的重复利用。

C++没有规定扩容因子K,这是由标准库的实现者决定的。

map与unordered_map

map中的元素是一些key-value对,关键字起索引作用,值表示和索引相关的数据。

底层实现

map底层是基于红黑树实现的,因此内部元素排列是有序的。

而unordered_map底层则是基于哈希表实现的,因此元素排列是杂乱无序的。

map

优点:

有序性,这是map结构的最大优点,其元素的有序性在很多应用中都会简化很多的操作。

map的查找、删除、增加等一系列操作时间复杂度稳定,都为O(logn)。

缺点:

查找、删除、增加等操作平均时间复杂度较慢,与n相关。

unordered_map

优点:
查找、删除。添加的速度快,时间复杂度为常数级O(1)。

缺点:

因为unordered_map内部基于哈希表,以(key, value)对的形式存储,因此空间占用率高。

unordered_map的查找、删除、添加的时间复杂度不稳定,平均为O(1),取决于哈希函数。极端情况下可能为O(n)。

《Effective STL》

1. 慎重选择容器类型

  1. 需要在容器任意位置插入元素,就选择序列容器(vector string deque list)
  2. 不关心容器中的元素是否是排序的,哈希容器可行
  3. 你选择的容器是C++标准的一部分,就排除了哈希容器,slist(单链表)和rope(“重型”的string)
  4. 随机访问迭代器:vector,deque,string,rope,双向迭代器:避免使用slist与哈希容器的一个实现
  5. 当发生元素的插入和删除,避免移动原来的容器的元素移动很重要,那么避免使用连续内存的容器
  6. 需要兼容C,就用vector
  7. 元素的查找速度为关键:哈希容器,排序的vector,标准关联容器(按速度排序)
  8. 介意引用计数,就要避免string与rope
  9. 回滚能力(错了能改):就要基于节点的容器。if对多个元素的插入操作需要事务语义就list;使用连续内存的容器也可以获得事务语义
  10. 基于节点的容器不会使迭代器、指针、引用变为无效
  11. 在string上使用swap会使迭代器、指针或引用变为无效
  12. 如果序列容器的迭代器是随机访问,只要没有删除操作发发生,且插入操作只在末尾,则指向数据的引用和指针不会失效 -deque

2. 不要试图编写独立于容器的代码

试图编写对序列容器和关联容器都适用的代码。

3. 确保容器中的对象拷贝正确且高效

  1. 存入容器的是你的对象的拷贝
  2. 剥离问题:向基类对象的容器添加派生类对象会导致,派生类对象所特有的信息被抹去。智能指针是解决这个问题的好办法。

4. 调用empty而不是检查size()是否为0

empty()对所有的标准容器都是常数时间操作,而size()对于list耗费的是线性时间。

5. 区间成员函数优先于之对应的单元素成员函数

好处:效率高易于理解,更能表达意图

区间创建、删除、赋值(assign)、插入可以用到区间成员函数

6. 当心C++编译器的烦人的分析机制-尽可能的解释为函数声明

/*三种同样形式的声明*/
int f(double d);
int f(double (d));
int f(double);/*g是⼀个函数,该函数的参数是⼀个指向不带任何参数的函数的指针*/
int g(double (* pf) ());
int g(double pf ());
int g(double ());list <int>data(istreamiterator<int>(datafile),istreamiterator<int>());
// 由上⾯分析可知,这是⼀个函数声明,跟我们想要做的事,⼤相径庭
// 第⼀个参数的名称是datafile ,其括号可省略,类型是istream_iterator<int>
// 第⼆个参数是⼀个函数指针,返回⼀个istream_iterator<int>
class Weight{...};
Weight w();

这是我们开始学习类的时候容易犯下的错误,我们想声明一个Weight函数,先进行默认初始化,编译器缺却我们了一个函数声明。 该函数不带任何参数,并返回一个Weight。

解决方法1:给函数参数加上括号

list<int>data((istreamiterator<int>(datafile), istreamiterator<int>());

解决方法2:避免是使用匿名的istream_iterator迭代器对象,而是给这些迭代器一个名称更好。

ifstream datafile("ints.dat");
istream_iterator<int> dataBegin(datafile);
istream_iteraotr<int> dataEnd;
list<int>data(dataBegin, dataEnd);

7.容器包含指针

如果在容器中包含了通过new操作创建的对象的指针,切记在容器对象调用析构函数之前将指针delete掉

解决方法:

最简单的方法用智能指针代替指针容器,这里的智能指针通常是被引用计数的指针

void dosomething() {typedef boost::shardptr <Weight> SPW; //令SPW=shardptr<Weight>vector <SPW> vwp;for (int i = 0; i < SOMEMAGICNUMBER; ++i) {vwp.push_back(SPW(new Weight))//这⾥不会发⽣内存泄露,即使前⾯抛出异常。...}
}

8. 切勿创建包含auto_ptr的容器

无论是被拷贝还是被复制,源对象都将失去对其资源的所有权。

而STL容器又是需要元素具有拷贝可赋值的属性的。

9. 当你复制一个auto_ptr时

它所指的对象的所有权被移交到复制的对象,而他自身被设置为NULL

10. 慎重选择删除元素的方法

10.1 要删除容器中有特定值的所有对象

如果容器是vector、string或deque,则使用erase-remove

如果是list 则使用list::remove

如果容器是一个关联容器,则使用它的erase成员函数

10.2 要在循环内部做某些(除了删除对象的操作之外)操作

如果容器是一个标准序列容器,则写一个循环来遍历容器中的元素,记住每次调用erase时,要用它的返回值更新迭代器。

如果是关联容器,写一个循环来遍历容器中的元素,记住当把迭代器传给erase时,要对他进行后缀递增。

for(specalcontainer<int>::iterator i=c.begin();i!=c.end()) {if(badvalue(*i)){logFile<<"..."<<*i<<endl;i=c.erase(i);//对于vector,string,deque删除的⼀个元素不仅会导致这个元素的迭代器失效,同时会导致所有 的迭代器失效,所以必须更新迭代器。} else {++i;}
}

要删除容器中满足判别式的所有对象

  1. 如果容器是vector、string或deque,则使用erase-remove_if
  2. 如果是list,则使用list::remove_if
  3. 如果是关联容器,则使用removecopyif和swap(把我们需要的值复制到新容器,然后交换容器),或者写一个循环来编译容器中的元素,记住当把迭代器传给erase时,要对他进行后缀递增
container<int>c;...
for(container<int>::iterator i=c.begin();i!=c.end();) {if(badvalue(*i)) c.erase(i++);//当该元素被删除的时候,该元素所有的迭代器都会失效,所以我们使⽤i++else ++i;
}

11. 了解分配子的约定与概念

分配子最初是作为内存模型的抽象,后来为了有利于开发作为对象形式的内存管理器,STL内存分配子负责分配和释放内存

1. 首先分配子能够为它所定义的内存模型中的指针和引用提供类型的定义

分别为allocator<T>::pointer与allocator<T>::reference,用户定义的分配子也应该提供这些类型定义,创建这种具有引用行为特点的对象是使用代理对象的一个例子,而代理对象会导致很多问题。

2. 库实现者可以忽略类型定义而直接使用指针和引用

允许每个库实现者假定每个分配子的指针类型等同于T*,引用为T&

3. STL实现者可以假定所有属于同一类型的分配子都是等价的

4. 大多数标准容器从来没有单独使用过对应的分配子

例如list,当我们添加一个节点的时候我们并不是需要T的内存,而是需要包含T的listNode的内存

所以说list从未需要allocator做任何内存分配,该list的分配子不能够提供list所需的分配内存的功能

所以它会利用分配子提供的一个模板,根据list中T来决定listNode的分配子类型为:Alloactor::rebind::other。这样就得到listNode的分配子,就可以为list分配内存

new与allocator在分配内存的时候,他们的接口不同

void operator new(sizet bytes);
pointer allocator<T>::allocator(sizetyoe numberjiects);
// pointer是个类型定义总是T
// 两者都带参数说名要分配多少内存,但是new 是指明⼀定的字节
// ⽽allocator ,它指明的内存中要容纳多少个T对象
// new返回的是⼀个void,⽽allocator<T>::allocate返回的是⼀个T
// 但是返回的指针并未指向T对象
// 因为T为被构造
// STL会期望allocator<T>::allocate的调⽤者最终在返回的内存中创建⼀个或者多个T对象

12.编写自定义分配子需要什么?

你的分配子是个模板,T代表为它分配对象的类型

提供模板类型定义,分别为allocator::pointer与allocator::reference

通常,分配子不应该有非静态对象

new返回的是一个void,而allocator::allocate返回的是一个T,但是返回的指针并未指向T对象,因为T为被构造,STL会期望allocator::allocate的调用者最终在返回的内存中创建一个或者多个T对象

一定要提供rebind模板

13. 理解分配子的用法

把STL容器中的内容放在共享内存中

把STL容器中的内容放到不同的堆中

14. 切勿对STL容器的线程安全性有不切实际的依赖

STL自身对多线程的支持非常有限

在需要修改STL容器或者调用STL算法时需要自己加锁

为了实现异常安全,最好不要手动加锁解锁,多使用RAII

15. 当你在动态分配数组的时候,请使用vector和string

16. 使用reserve来避免不必要的重新分配

STL容器会自动增长以便容纳下其中的数据,只要没有超出他们的限制

vector与string的增长实现过程:

分配一块大小为旧内存两倍的新内存,把容器中所有的元素复制到新内存中,析构旧内存的对象,释放旧内存。

reserve函数能够把你重新分配内存的次数减到最小,从而避免重新分配和指针、迭代器、引用失效带来的开销,所有应该尽早的使用reserve,最好是容器在被刚刚构造出来的时候就使用

4个易混函数(只有vector与string提供所有这四个函数)

  1. size():告诉你容器中有多少个元素
  2. capacity():告诉你该容器利用已分配的内存能够容纳多少个元素,这是容器能够容纳元素的总数
  3. resize(Cotainer::size_type n):强迫容器改变到包含n个元素的状态,如果size返回的数<n,则容器尾部的元素就会被析构,如果>n,则默认构造新的元素添加到容器的末尾,如果n要比当前的容器容量大,那么就会在添加元素之前,重新分配内存
  4. reserve(Container::size_type n),强迫容器改变容量变为至少n,前提是不比当前的容量小,这会导致重新分配。

有两种方式避免不必要的内存分配

  1. 你提前已经知道要用多少的元素,此时就可以使用reserve。
  2. 先预留足够大的空间,然后再去除多余的容量(如何去除,参照swap技巧)

17. string实现的多样性

  1. string的值可能会被引用计数
  2. string对象的大小可能是char*的大小的1~7倍
  3. 创建一个新的字符串可能会发生0,1,2次的动态分配内存
  4. string也可能共享其容量,大小信息
  5. string可能支持针对单个对象的分配子
  6. 不同的实现对字符内存的最小分配单位有不同的策略

18. 了解如何把vector和string数据传给旧的API

if(!v.empty())
do something(&v[0],v.size());
or dosomething(v.c_str());

如何用自C API的元素初始化一个vector

sizet fillArray(doublepArray,size_t arraySize);
vector< double >vd(maxnumbers);
vd.resize(fillArray(&v[0],vd.size()));
sizet fillString(charpArray,size_t arraySize);
vector< char >vc(maxnumbers);
size_t charWritten=fillString(&v[0],vd.size(0))
string s(vc.begin(),v.end()+charWrittrn)

19. 使用“swap技巧”删除多余的容量

vector<C> cs.swap(cs);
string s;
string (s).swap(s);

swap还可以删除一个容器

vector< C>().swap(cs);
string s;
string().swap(s);

20. swap的时候发生了什么

在swap的时候,不仅两个容器的元素被交换了,他们的迭代器指针和引用依然有效(string除外),只是他们的元素已经在另一个容器里。

21. 避免使用vect<bool>,用deque<bool>和bitset代替它

对于vector来说:

第一,它不是一个STL容器。

第二,它不容纳bool。除此以外,就没有什么要反对的。

22. 理解等价与相等

相等基于operator==,一旦x==y则返回真,则x与y相等

等价关系是在已经排好序的区间中对象值的相对顺序,每一个值都不在另一个值的前面。

!=(x<y)&&!=(y<x)。每个标注关联容器的比较函数是用户自定义的判别式,每个标准关联容器都是通过key_comp成员函数使序列判别式可被外部使用

23. 熟悉非标准散列容器

  1. hashmap
  2. hashset
  3. hashmultimap
  4. hashmultiset

24. 为包含指针的关联容器指定比较类型,而不是比较函数,最好是准备一个模板

struct Dfl {template<typename ptrtype>bool operator()(ptrtype pT1, ptrtype pT2) const {return pT1 < pT2;}
}

25. 切勿直接修改set或multiset中的键

set/multiset的值不是const,map/multimap的键是const

如何修改元素:

  1. 找到想要修改的元素
  2. 为将要修改的元素做一份拷贝。在map/multimap的情况下,不要把该拷贝的第一部分声明为const
  3. 修改拷贝
  4. 把该元素从容器中删除,一般用erase
  5. 把新值插入容器

26.考虑用排序的vector替代关联容器

当程序使用数据结构的方法是:设置阶段、查找阶段、重组阶段,使用排序的vector容器可能比使用关联容器的效率要更好一点(当在使用数据结构的时候,查找操作不与删除添加操作混在一起的时候考虑vector)

好处:消耗更少的内存,运行更快一些

注意:

当你使用vector来模仿map<const k, v>时,存储在vector中的是pair<k,v>,而不是pair<const k, v>;需要自己写3个自定义比较函数(用于排序的比较函数,用于查找的比较函数)

typedef pair<string, int> Data;
class Datacompare {
public:bool operator()(const Data &lhs, const Data &rhs) const {return keyless(lhs.first, rhs.first)}//⽤于排序的⽐较函数bool operator()(const Data &lhs, const Data::first_type &k) const {return keyless(lhs.first, k)}//⽤于查找的⽐较函数bool operator()(const Data::first_type &k, const Data &rhs) const {return keyless(k, rhs.first)}//⽤于查找的⽐较函数private:bool keyless(const Data::firsttype &k1, const Data::firsttype &k2) const {return k1 < k2;}//为了保证operator()的⼀致性
}

27. 更新一个已有的映射表元素

如果要更新一个已有的映射表元素,则应该选择operator[],如果是添加元素,那么最好还是选择insert。

28.  iterator

iterator优先于constiterator,reserveiterator,constreserveiterator。

29.使用distance和advance将容器的const_iterator转换为iterator

typedef deque<int> IntDeque;
typedef IntDeque::iterator Iter;
typedef IntDeque::const_iterator ConstIter;
IntDeque d;
ConstIter ci;
...
Iter i(d.begin());
advance(i, distance<ConstIter>(i, ci));

30. 正确理解由reserve_iteratorr的base()成员函数所产生的iterator的用法

1. 对于插入操作,ri和ri.base()是等价的

2. 对于删除操作,ri和ri.base()是不等价的

3. v.erase((++ri).base());

31. istreambuf_iterator

对于逐个字符的输入请考虑使用istreambuf_iterator

  1. ifstream inputFile("sdsdsa.txt");
  2. string filedata((istreambufiterator<char>(inputFile),istreambufiterator<char>());

32. 如果使用的算法需要指定一个目标空间

如果所使用的算法需要指定一个目标空间,确保目标区间足够大或确保它会随着算法的运行而增大。

要在算法执行过程中增大目标区间,请使用插入型迭代器:backinserter,frontinserteer,ostream_iterator【插入器,它接收一个容器,生产一个迭代器,能实现向给定容器添加元素】

33. 了解各种与排序相关的选择

  1. 如果需要对vector,string,deque或者数组中的元素进行一次完全排序,那么可以使用sort和stable_sort
  2. 如果有一个vector,string,deque或者数组,并且只需要对等价性最前面的n个元素进行排序,那就可以使用partial_sort
  3. 如果有一个vector,string,deque或者数组,并且只需要找到第n个位置上的元素,或者,并且只需要找到性价比最前面的n个元素,并不需要排序,那么nth_element就行
  4. 如果需要将一个标准容器中的元素按照是否满足某个特定的区间分开,那么就选择partition、stable_partition
  5. 如果你的数据在list中,那么你可以选择内置的sort和stablesort算法。同时如果你需要获得partitionsort或nth_element算法的效果可以采用一些间接的方法
  6. 对于排序算法的选择应该基于功能选择,而不是性能

34. 如果要删除元素,需要在remove后面使用erase

  1. remove并没有删除容器中的元素:因为remove它不是成员函数(list除外),所以它不知道要删除哪个容器的元素。它会把不被删除的元素排在前面,要删除的元素排在后面,所以它返回的一个指针指向最后一个不被删除的元素的后面的那个元素
  2. 我们就要使用在remove后面使用erase函数。v.erase(remove(v.begin(), v,end(),element)),w.end()
  3. 还有两类函数也是这种情况:remove_if,unique;
  4. 但是list函数把remove与erase结合在一起生产的list::remove比原先的remove-erase的效率高。unique和remove_if同理

35.对包含指针的容器使用remove这一类算法一定要小心

原因:由于remove是将那些要被删除的指针被那些不需要被删除的指针覆盖了,所有没有指针指向那些被删除指针所指向的内存和资源,所有资源就泄漏了

做法:使用智能指针或者在使用remove-erase之前手动删除指针并置空

36. 了解那些算法要求使用排序的区间作为参数

template<typename InputIterator
typename OutputIterator
typename predicate>
OutputIterator copy_if(InputIterator begin, InputIterator end, OutputIterator
destbegin, predicate p) {while (begin != end) {if (p(*begin))destbegin++ = begin;++begin;}return destbegin;}

37. 使用accumulate或者for_each进行区间统计

  1. accumulate(innerproducet、adjacentdifference、partial_sum)位于<numeric>中
  2. for_each(区间,函数对象)
  3. accumulate(begin,end,初始值);accumulate(初始值,统计函数)

38. 遵循按值传递的原则来设计函数子类

如果做能够允许函数对象可以很大,或者保留多态,又可以与STL所采用的按值传递函数指针的习惯保持一致:将数据和虚函数从函数子类中分离出来,放到一个新的类中;然后在函数子类中包含一个指针,指向这一个新类的对象。

template<typename T>
class Bs : public:unary_function<T, void> {
private:Weight w;int x;
...
virtual ~Bs();
virtual void operator()(const T &val) const;
friend class B<T>
}template<typename T>
class B : public:unary_function<T, void> {
private:BS <T> *p;
public:virtual void operator()(const T &val) const {p->operator()(val);}
}

41. 确保判别式是“纯函数”

对于用作判别式的函数对象,使用时它会被拷贝存起来,然后再使用这个拷贝。这一特性要求判别式函数必须为纯函数。

42. 使你的函数子类可配接

为什么:

  1. 可配接的函数对象能够与其他STL组件默契的协同工作
  2. 能够让你的函数子类拥有必要的类型定义
  3. 四个标准的函数配接器(not1,not2,bind1st,bind2nd)要求这些类型定义

为什么not1等要这些定义:能够辅助他们完成一些功能

如何使函数可配接:让函数子从特定的基类继承:unaryfunction与binaryfunction

注意:unaryfunction<operator所带参数类型,返回类型>binaryfunction<operator 1, operator 2,返回类型>

43. 理解ptrfun && memfun && memfunref

在函数和函数对象被调用的时候,总是使用非成员函数形式发f(),而当你使用成员函数的形式时x.f(), p->f();将不通过编译,所以使用上面的那些就能调整成员函数,使其能够以成员函数的形式调用函数和函数对象。

每次将成员函数传给STL组件的时候,就要使用他们。

44. 确保less<T>与operator<的语义相同

一般情况下我们使用less<T>都是默认通过operator<来排序。

如果你想要实现不同的比较,最好是重新写一个类,而不是修改less

45. 算法的调用优先于手写的循环

  1. 效率高
  2. 手写的循环容易出错
  3. 算法的代码更简单明了,易于维护

46. 容器的成员函数优先于同名函数

  1. 成员函数往往速度快
  2. 成员函数通常与容器结合更紧密,是算法所不能比的。

47. 正确区分以下关键字:

count、find、binarysearch、lowerbound、upperbound、equalrange

48. 使用函数对象作为STL算法的参数

49.避免产生“直写行”的代码

50. 包含正确的头文件

  1. 几乎所有的STL容器都被声明在与之同名的头文件中
  2. 除了四个STL算法外,其他所有算法都被声明在<algorithm>中;accumulate(innerproducet、adjacentdifference、partial_sum)位于<numeric>中
  3. 特殊类型的迭代器(istreamiterator,istreambufiterator)被声明在<iterator>
  4. 标注的函数子(less< T >)和函数配接器< not1,bind2nd >b被声明在头⽂件< funtional >中

51. 学会分析于STL相关的编译器的诊断信息

52. 熟悉于STL相关的web站点

  1. SGI STL
  2. STLport
  3. Boost

《Effective C++》

条款16:成对使用new和delete。

new创建,delete删除

new[]创建,[]delete删除

条款30:了解inline的里里外外

  • inline函数,对函数的每一个调用都用函数本体替代,调用不承受额外开销,编译器对其执行语境相关最优化。增加目标码大小,额外的换页行为,降低缓存击中率,效率损失。
  • 对虚函数进行inline无意义,虚函数是运行时确定,inline是在编译期替换。
  • 编译器一般不对“通过函数指针进行调用”提供inline,是否inline取决于调用的方式

条款34:区分接口继承和实现继承

public继承由函数接口继承+函数实现继承组成

纯虚函数两个特性:1. 它们必须被任何继承了它们的具象class重新声明;2. 在抽象class中通常没有定义

声明一个纯虚函数的目的是为了让派生类只继承函数接口。

只提供接口,派生类根据自身去实现。

声明非纯虚函数的目的是让派生类继承函数的接口和缺省实现。

必须支持一个虚函数,如果不想重新写一个(override),可以使基类提供的缺省版本。

声明非虚函数的目的是令派生类继承函数接口和一份强制性实现。

任何派生类都不应该尝试修改此函数,non-virtual函数代表不变性>特异性,不应该在派生类被重新定义。

条款35:考虑virtual函数以外的选择

  1. non-virtual interface, 以public non-virtual成员函数包裹较低访问性(private/protected)的虚函数。
  2. virtual函数替换成“函数指针成员变量”。
  3. tr1::function成员替换virtual。

条款36:绝不重新定义继承而来的非虚函数

public继承说明,每个派生类对象都是基类对象,非虚函数(静态绑定)一定会继承基类的接口和实现。

重新定义则设计出现矛盾。派生类重新定义使得出现特化,这样就不一定适用于基类,那么就不应该public。

条款37:绝不重新定义继承而来的缺省参数值

因为缺省参数值是静态绑定,虚函数是动态绑定。

静态类型是程序中被声明时采用的类型,动态类型是目前所指对象的类型。

条款39:明智而谨慎的使用private继承

如果派生类需要访问基类保护的成员,或需要重新定义继承而来的虚函数,采用private继承。

条款40:明智而谨慎的使用多重继承

多继承中实现派生类中只有一份数据,虚继承。

虚继承会增加大小,速度,初始化等成本。

最好不要使用虚继承或虚基类中不放置数据。

条款44:将与参数无关的代码抽离template

template生成多个class和多个函数,所以任何template代码都不应该与某个造成膨胀的template参数产生依赖关系。

因非类型模板参数(non-type template parameters)而造成代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template参数。

因类型参数(type parameters)而造成的代码膨胀,往往可降低,做法是让带有相同二进制表述的具现类型共享实现代码。

条款45:运用成员函数模板接受所有兼容类型

如果你声明member template用于“泛化copy构造”或“泛化assignment操作”,还是需要声明正常的copy构造函数和copy assignment操作符。

条款46:需要类型转换时请以模板定义非成员函数

当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些参数定义为“class template内部的friend函数”。

条款47:请使用traits classes表现类型信息

Traits classes使得“类型相关信息”在编译期可用。

它们以templates和“template特化”完成实现。整合重载技术后,traits classes有可能在编译期对类型执行if...else测试

条款48:认识template元编程

template metaprogramming(TMP,模板元编程)可将工作由运行期移往编译期,因而得以实现早期错误侦测和更好的执行效率。

TMP可被用来生成“基于政策选择组合”(based on combinations of policy choices)的客户定制代码

也可用来避免生成对某些特殊类型并不合适的代码。

泛型编程

C++模板全特化和偏特化

模板分为类模板和函数模板,特化分为特例化(全特化)和部分特例化(偏特化)。

对模板特例化是因为对特定类型,可以利用某些特定知识提高效率,而不是使用通用模板。

对函数模板:

  1. 模板和特例化版本应该声明在同一头文件,所有同名模板的声明应放在前面,接着是特例化版本。
  2. 一个模板被称为全特化的条件:1.必须有一个主模板类 2. 模板类型被全部明确化。

模板函数:

template<<typename T1, typename T2>
void funT1 a, T2 b)
{cout<<"模板函数"<<endl; }template<>
void fun<int , char >(int a, char b)
{cout<<"全特化"<<endl;
}

模板函数,只有全特化,偏特化的功能可以通过函数的重载完成。

对类模板:

template<typename T1, typename T2>
class Test
{
public:Test(T1 i,T2 j):a(i),b(j){cout<<"模板类"<<endl;}private:T1 a;T2 b;
};template<>
class Test<int , char> {
public:Test(int i, char j):a(i),b(j){cout<<"全特化"<<endl;}
private:int a;char b;
};template <typename T2>
class Test<char, T2> {
public:Test(char i, T2 j):a(i),b(j){cout<<"偏特化"<<endl;}
private:char a;T2 b; }

对主版本模板类、全特化类、偏特化类的调用优先级从高到低排序:全特化类>偏特化类>主版本模板类。

C++11新特性

1. 类型推导

1. auto

auto可以让编译器在编译器就推导出变量的类型

  1. auto的使用必须马上初始化,否则无法推导出类型
  2. auto在一行定义多个变量时,各个变量的推导不能产生二义性,否则编译失败
  3. auto不能用作函数参数
  4. 在类中auto不能用作非静态成员变量
  5. auto不能定义数组,可以定义指针
  6. auto无法推导出模板参数
  7. 在不声明为引用或者指针时,auto会忽略等号右边的引用类型和cv限定
  8. 在声明为引用或指针时,auto会保留等号右边的引用和cv属性

2. decltype

decltype则用于推导表达式类型,这里只用于编译器分析表达式的类型,表达式实际不会计算

decltype不会像auto一样忽略引用和cv属性,decltype会保留表达式的引用和cv属性

对于decltype(exp)有:

  1. exp是表达式,decltype(exp)和exo类型相同
  2. exp是函数调用,decltype(exp)和函数返回值类型相同
  3. 其他情况,若exp是左值,decltype(exp)是exp类型的左值引用

auto和decltype的配合使用

template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {return t + u;
}

2. 右值引用

左值右值:

左值:可以放在等号左边,可以取地址并有名字

右值:不可以放在i等号左边,不能去地址,没有名字

字符串字面值“abcd”也是左值,不是右值

++i、--i是左值,i++、i--是右值

1. 将亡值

将亡值是指C++新增的和右值引用相关的表达式

将亡值可以理解为即将要销毁的值,通过“盗取”其他变量内存空间方式获取的值,在确保其他变量不再被使用或者即将被销毁时,可以避免内存空间的释放和分配,延长变量值的生命周期,常用来完成移动构造或者移动赋值的特殊任务

2. 左值引用

左值引用就是对左值进行引用的类型,是对象的一个别名

并不拥有所绑定对象的堆存,所以必须立即初始化。对于左值引用,等号右边的值必须可以取地址,如果不能取地址,则会编译失败,或者可以使用const引用形式

3. 右值引用

表达式等号右边的值需要是右值,可以使用std::move函数强制把左值转换为右值。

4. 移动语义

可理解为转移所有权,对于移动语义,类似于转让或者资源窃取的意思,对于那块资源,转为自己所拥有,别人不再拥有也不会再使用。

通过移动构造函数使用移动语义,也就是std::move;移动语义仅针对于那些实现了移动构造函数的类的对象,对于那些基本类型int、float等没有任何优化作用,还是会拷贝,因为它们实现没有对应的移动构造函数

浅拷贝:

a和b的指针指向了同一块内存,就是浅拷贝,只是数据的简单赋值;

深拷贝:

深拷贝就是再拷贝对象时,如果被拷贝对象内部还有指针引用指向其他资源,自己需要重新开辟一块新内存存储资源

5. 完美转发

写一个接受任意实参的函数模板,并转发到其他函数,目标函数会收到与转发函数完全相同的实参,通过std::forward()实现

nullptr

nullptr是用来代替NULL,一般C++会把NULL、0视为同一种东西,这取决于编译器如何定义NULL,有的定义为((void*)0),有的定义为0

C++不允许直接将void*隐式的转换为其他类型,在进行C++重载时会发生混乱

例如:

void foo(char *);
void foo(int );

如果NULL被定义为((void*)0),那么当编译char *ch = NULL时,NULL被定义为0

当foo(NULL)时,此时NULL为0,会去调用foo(int),从而发生混乱

为解决这个问题,从而需要使用NULL时,用nullptr代替:

C++11引入nullptr关键字来区分空指针和0。nulllptr的类型为nullptr_t,能够转换为任何指针或成员指针的类型,也可以进行相等或不等的比较。

范围for循环

基于范围的迭代写法,for(变量:对象)表达式

对string对象的每个字符做一些操作:

string str("some thing");
for(char c:str) cout << c << endl; //对字符串str中的每个字符进行cout操作。

对vector进行遍历:

std::vector<int>arr(5, 100);for(std::vector<int>::iterator i = arr.begin(); i != arr.end(); ++i) {std::cout << *i << std::endl;}//范围for:for(auto &i : arr) {std::cout << i << std::endl; }

列表初始化

C++定义了几种初始化方式,例如对一个int变量x初始化为0:

int x = 0; //method1
int x = {0}; // method2
int x{0}; // method3
int x(0); // method4

采用花括号来进行初始化称为列表初始化,无论是初始化对象还是为对象赋新值。

用于对内置类型变量时,如果使用列表初始化,且初始值存在丢失信息风险时,编译器会报错。

long double d = 3.1415926536;
int a = {d}; //存在丢失信息⻛险,转换未执⾏。
int a = d; //确实丢失信息,转换执⾏。

lambda表达式

lambda表达式表示一个可调用的代码单元,没有命名的内联函数,不需要函数名因为我们直接(一次性的)用它,不需要其他地方调用它。

1. lambda表达式的用法:

[capture list] (parameter list) -> return type {function body }
// [捕获列表] (参数列表) -> 返回类型 {函数体 }
// 只有 [capture list] 捕获列表和 {function body } 函数体是必选的auto lam =[]() -> int { cout << "Hello, World!"; return 88; };
auto ret = lam();
cout<<ret<<endl; // 输出88

->int:代表此匿名函数返回int,大多数情况下lambda表达式的返回值可由编译器猜测得出,因此不需要我们指定返回值类型。

2. lambda表达式的特点:

(1)变量捕获才是成就lambda卓越的秘方

  1. []不捕获任何变量,这种情况下lambda表达式内部不能访问外部的变量
  2. [&]以引用方式捕获所有变量(保证lambda执行时变量存在)
  3. [=]用值的方式捕获所有变量(创建时拷贝,修改对lambda内对象无影响)
  4. [=, &foo]以引用捕获变量foo,但其余变量都靠值捕获
  5. [&, foo]以值捕获foo,但其余变量都靠引用捕获
  6. [bar]以值方式捕获bar;不捕获其他变量
  7. [this]捕获所在类的this指针
int a = 1, b =2, c = 3;
auto lam2 = [&, a]() {    //b, c以引用捕获,a以值捕获b = 5; c = 6;         //a = 1; a不能赋值cout << a << b << c << endl; //输出 1 5 6
};
lam2();
//值捕获
void fcn()
{size_t v1 = 42;auto f = [v1] {return v1;};v1 = 0;auto j = f();    //j = 42; 创建时拷贝,修改对lambda内对象无影响
}
//可变lambda
void fcn()
{size_t v1 = 42;auto f = [v1] () mutable {return ++v1;}; //修改值捕获可以加mutablev1 = 0;auto j = f();    //j = 43
}  
//引用捕获
void fcn()
{size_t v1 = 42;    //非constauto f = [&v1] () {return ++v1;};v1 = 0;auto j = f();    //注意此时 j = 1;
}

(2)lambda最大的一个优势是在使用STL中的算法(algorithms)库

例如:数组排序

int arr[] = {6, 4, 3, 2, 1, 5};
bool compare(int& a, int& b)    //谓词函数
{return a > b;
}std::sort(arr, arr + 6, compare);lambda形式:
std::sort(arr, arr + 6, [](const int a, const int b){return a > b;});//降序排序
std::for_each(begin(arr), end(arr), [](const int& e){cout << "After:" << e << endl;});
//6 5 4 3 2 1

并发

1. std::thread

default(1)

thread() noexcept;

initialization(2) template<class Fn, class... Args> explicit thread (Fn&& fn, Args&&... args);
copy[deleted](3) thread (const thread&) = delete;
move(4) thread (thread&& x) noexcept;
  1. 默认构造函数,创建一个空的thread执行对象。
  2. 初始化构造函数,创建一个thread对象,该thread对象可被joinable,新产生的线程会调用fn函数,该函数的参数由args给出。
  3. 拷贝构造函数(被禁用),意味着thread不可被拷贝构造。
  4. move构造函数,调用成功之后x不代表任何thread执行对象

注意:可被joinable的thread对象必须在他们销毁之前被主线程join或者将其设置为detached。

std::thread在使用上容易出错,即std::thread对象在线程函数运行期间必须是有效的,即:

#include <iostream>
#include <thread>void threadproc()
{while(true) {std::cout << "I am New Thread!" << std::endl;}
}void func()
{std::thread t(threadproc);
}int main() {func();while(true) {} //让主线程不要退出return 0;
}

以上代码在main函数中调用了func函数,在func函数中创建了一个线程,乍一看没啥问题,但是一运行就会崩溃。

崩溃的原因是:在func函数调用结束后,func中局部变量t(线程对象)被销毁,而此时线程函数仍在运行中。所以在使用std::thread类时,必须保证线程函数运行期间其线程对象有效。

std::thread对象提供了一个detach方法,通过这个方法可以让线程对象与线程函数脱离关系,这样即使线程对象被销毁,也不影响线程函数的运行。

只需要在func函数中调用detach方法即可,代码如下:

// 其他代码保持不变
void func() {std::thread t(threadproc);t.detach();
}

2. lock_guard

lock_guard是一个互斥量包装程序,它提供了一种方便的RAII(Resource acquisition is initialization)风格的机制来在作用域块的持续时间内拥有一个互斥量。

创建lockguard对象时,它将尝试获取提供给它的互斥锁的所有权。当控制流离开lockguard对象的作用域时,lock_guard析构并释放互斥量。

它的特点如下:

  1. 创建即加锁,作用域结束自动析构并解锁,无需手动解锁
  2. 不能中途解锁,必须等作用域结束才解锁
  3. 不能复制

3. unique_lock

unique_lock是一个通用的互斥量锁定包装器,它允许延迟锁定,限时深度锁定,递归锁定,锁定所有权的转移以及与条件变量一起使用。

简单地说,unique_lock是lock_guard的升级加强版,拥有lock_guard的所有功能,同时又具有其他很多方法,使用起来更加灵活方便,能够应对更加复杂的锁定需要。

特点如下:

  1. 创建时可以不锁定(通过指定第二个参数为std::after_lock),而在需要时再锁定
  2. 可以随时加锁解锁
  3. 作用域规定同lock_guard,析构时自动释放锁
  4. 不可复制,移动
  5. 条件变量需要该类型的锁作为参数(此时必须使用unique_lock)

八股面经总结-C++相关推荐

  1. 【转】SVM入门(一)SVM的八股简介

    (一)SVM的八股简介 支持向量机(Support Vector Machine)是Cortes和Vapnik于1995年首先提出的,它在解决小样本.非线性及高维模式识别中表现出许多特有的优势,并能够 ...

  2. 【神经网络八股扩展】:数据增强

    课程来源:人工智能实践:Tensorflow笔记2 文章目录 前言 TensorFlow2数据增强函数 数据增强+网络八股代码: 总结 前言 本讲目标:数据增强,增大数据量 关于我们为何要使用数据增强 ...

  3. 八股总结(二)计算机网络与网络编程

    layout: post title: 八股总结(二)计算机网络与网络编程 description: 八股总结(二)计算机网络与网络编程 tag: 八股总结 文章目录 计算机网络 网络模型 网络体系结 ...

  4. 在Android面试前背八股和学面试技巧真的有用吗?

    前言: 今年秋招以来,我集中面试了一些公司,想着至少能过一家吧,但后面发现面试安排十分紧凑,有种顾此失彼的感觉. 我刚开始的时候对Android面试的具体情况全然不知,也没有人告诉我应该注意些什么,可 ...

  5. 面试官:hold住了八股和算法,扫码登录应该怎么实现你总不会了吧

    真实面试小场景: 经过八股和算法的交锋,老三松了口气,都hold住了.只见面试官微微一笑,"其实,我真正想问的是--你觉得扫码登录应该怎么实现." 老三:"啊--这个,哦 ...

  6. 流利地回答出面试官提出的八股问题,面试官却突然说“背得不错”,该怎么回答?

    面试前背题是大家心照不宣的做法,一般面试官也不会揭穿,但如果遇到一位犀利的面试官,那该怎么办呢? 一位网友就遇到了这样的窘境: 面试的时候,十分流利地回答出面试官提出的概念原理方面的问题,面试官却突然 ...

  7. 流利地回答出面试官提出的八股问题,面试官却突然说“背得不错”,该怎么回答?...

    面试前背题是大家心照不宣的做法,一般面试官也不会揭穿,但如果遇到一位犀利的面试官,那该怎么办呢? 一位网友就遇到了这样的窘境: 面试的时候,十分流利地回答出面试官提出的概念原理方面的问题,面试官却突然 ...

  8. 计算机网络学习笔记<一>|工作必备|银行科技岗面试|内附八股面经|秋招提前批冲冲冲

    计算机网络入门 一.基本概念 1.计算机网络组成 2.计算机网络分类 3.标准化工作 RFC(Request For Comments)因特网标准形式 ISO组织 OSI参考模型.HDLC协议 4.速 ...

  9. 拿下这些八股,能在寒冬找到工作吗

    拿下这些八股,能在寒冬找到工作吗??? 文章目录 拿下这些八股,能在寒冬找到工作吗??? 1.解决线程安全的问题 2.JVM有哪些内存区域 3.对空间大小怎么配置?各区域怎么划? 4.JVM内存区域会 ...

  10. 校招八股:C/C++开发工程师常见笔试、面试题目不完全汇总【很基础】

    这里汇总一些C/C++开发岗的常见面试八股题,都属于比较基础.偏理论性的题目.换句话说,如果这些题目答不上来,可能会给面试官留下的基础不好的印象,尤其是科班生哈. 废话不多说,直接开始. 一.C/C+ ...

最新文章

  1. “西南偏南” 三十年首次 “聚焦中国”
  2. 【.NET特供-第三季】ASP.NET MVC系列:传统WebForm站点和MVC站点执行机制对照
  3. Unity3D 装备系统学习Inventory Pro 2.1.2 基础篇
  4. LeetCode 497. 非重叠矩形中的随机点(前缀和+二分查找)
  5. python虚函数_virtual(虚函数) vtbl(虚函数表)与vptr(虚函数表指针)
  6. 学习Vim 全图解释
  7. Axure rp 8 基本用法图解之一
  8. 【案例分享】利用Python识别图片中的文字
  9. 用SPSS搞定问卷调查中的决断值
  10. 蓄电池充电c语言程序,蓄电池的充电方法和蓄电池工作原理
  11. 华为手机android是什么意思,华为手机里的文件夹表示什么意思?
  12. 2014腾讯实习生招聘武汉试题
  13. 一朵花的组成结构图_请问一朵完整的花由哪几部分组成
  14. 支付宝周期扣款Java逻辑代码
  15. 如何批量生成矩阵25码
  16. vue实现微信网页授权登录
  17. RxJava过滤操作符
  18. 现代化医院PACS/RIS系统概述
  19. 干货分享:小鸟云虚拟主机如何绑定域名及解析域名?
  20. 【Emmet 的使用手册(知识点超全版本)】

热门文章

  1. 双语播放器关键词研究
  2. 2.6内核编译配置选项简介--介绍make menuconfig中的每个选项含义
  3. GIS十问之五:如何获得最靠谱的招聘信息?
  4. 《iOS5 programming cookbook》学习笔记1
  5. 【记录篇】—— web字体的相关
  6. html中css与左侧距离,html 实现DIV+CSS 让左右内容之间保持一定距离
  7. (C语言)计算e的x次方
  8. 内容分发——插槽solt(重点)
  9. 【知识蒸馏】2021年最新知识蒸馏综述和论文总结
  10. 计算机网络维护工具,一种计算机网络设备维护用的工具箱的制作方法