C/C++开发既要代码小,又要速度快!程序该如何优化?

 

 

对程序进行优化,通常是指优化程序代码或程序执行速度。优化代码和优化速度实际上是一个予盾的统一。一般是优化了代码的尺寸,就会带来执行时间的增加;如果优化了程序的执行速度,通常会带来代码增加的副作用。很难鱼与熊掌兼得,只能在设计时掌握一个平衡点。 

一、程序结构的优化

1、程序的书写结构

虽然书写格式并不会影响生成的代码质量,但是在实际编写程序时还是应该尊循一定的书写规则,一个书写清晰、明了的程序,有利于以后的维护。在书写程序时,特别是对于While、for、do…while、if…else、switch…case 等语句或这些语句嵌套组合时,应采用“缩格”的书写形式。

2、标识符

程序中使用的用户标识符除要遵循标识符的命名规则以外,一般不要用代数符号(如a、b、x1、y1)作为变量名,应选取具有相关含义的英文单词(或缩写)或汉语拼音作为标识符,以增加程序的可读性,如:count、number1、red、work 等。

3、程序结构

C 语言是一种高级程序设计语言,提供了十分完备的规范化流程控制结构。因此在采用C 语言设计单片机应用系统程序时,首先要注意尽可能采用结构化的程序设计方法,这样可使整个应用系统程序结构清晰,便于调试和维护。

对于一个较大的应用程序,通常将整个程序按功能分成若干个模块,不同模块完成不同的功能。各个模块可以分别编写,甚至还可以由不同的程序员编写,一般单个模块完成的功能较为简单,设计和调试也相对容易一些。在C 语言中,一个函数就可以认为是一个模块。

所谓程序模块化,不仅是要将整个程序划分成若干个功能模块,更重要的是,还应该注意保持各个模块之间变量的相对独立性,即保持模块的独立性,尽量少使用全局变量等。对于一些常用的功能模块,还可以封装为一个应用程序库,以便需要时可以直接调用。但是在使用模块化时,如果将模块分成太细太小,又会导致程序的执行效率变低(进入和退出一个函数时保护和恢复寄存器占用了一些时间)。

4、定义常数

在程序化设计过程中,对于经常使用的一些常数,如果将它直接写到程序中去,一旦常数的数值发生变化,就必须逐个找出程序中所有的常数,并逐一进行修改,这样必然会降低程序的可维护性。因此,应尽量当采用预处理命令方式来定义常数,而且还可以避免输入错误。

5、减少判断语句

能够使用条件编译(ifdef)的地方就使用条件编译而不使用if 语句,有利于减少编译生成的代码的长度。

6、表达式

对于一个表达式中各种运算执行的优先顺序不太明确或容易混淆的地方,应当采用圆括号明确指定它们的优先顺序。一个表达式通常不能写得太复杂,如果表达式太复杂,时间久了以后,自己也不容易看得懂,不利于以后的维护。

7、函数

对于程序中的函数,在使用之前,应对函数的类型进行说明,对函数类型的说明必须保证它与原来定义的函数类型一致,对于没有参数和没有返回值类型的函数应加上“void”说明。如果果需要缩短代码的长度,可以将程序中一些公共的程序段定义为函数。如果需要缩短程序的执行时间,在程序调试结束后,将部分函数用宏定义来代替。注意,应该在程序调试结束后再定义宏,因为大多数编译系统在宏展开之后才会报错,这样会增加排错的难度。

 

8、尽量少用全局变量,多用局部变量

因为全局变量是放在数据存储器中,定义一个全局变量,MCU 就少一个可以利用的数据存储器空间,如果定义了太多的全局变量,会导致编译器无足够的内存可以分配;而局部变量大多定位于MCU 内部的寄存器中,在绝大多数MCU 中,使用寄存器操作速度比数据存储器快,指令也更多更灵活,有利于生成质量更高的代码,而且局部变量所的占用的寄存器和数据存储器在不同的模块中可以重复利用。

 

9、设定合适的编译程序选项

许多编译程序有几种不同的优化选项,在使用前应理解各优化选项的含义,然后选用最合适的一种优化方式。通常情况下一旦选用最高级优化,编译程序会近乎病态地追求代码优化,可能会影响程序的正确性,导致程序运行出错。因此应熟悉所使用的编译器,应知道哪些参数在优化时会受到影响,哪些参数不会受到影响。

二、代码的优化

1、选择合适的算法和数据结构

应熟悉算法语言。将比较慢的顺序查找法用较快的二分查找法或乱序查找法代替,插入排序或冒泡排序法用快速排序、合并排序或根排序代替,这样可以大大提高程序执行的效率。

选择一种合适的数据结构也很重要,比如在一堆随机存放的数据中使用了大量的插入和删除指令,比使用链表要快得多。数组与指针具有十分密切的关系,一般来说指针比较灵活简洁,而数组则比较直观,容易理解。对于大部分分的编译器,使用指针比使用数组生成的代码更短,执行效率更高。

 

但是在Keil 中则相反,使用数组比使用的指针生成的代码更短。

 

2、使用尽量小的数据类型

能够使用字符型(char)定义的变量,就不要使用整型(int)变量来定义;能够使用整型变量定义的变量就不要用长整型(long int),能不使用浮点型(float)变量就不要使用浮点型变量。当然,在定义变量后不要超过变量的作用范围,如果超过变量的范围赋值,C 编译器并不报错,但程序运行结果却错了,而且这样的错误很难发现。

 

3、使用自加、自减指令

通常使用自加、自减指令和复合赋值表达式(如a-=1 及a+=1 等)都能够生成高质量的程序代码,编译器通常都能够生成inc 和dec 之类的指令,而使用a=a+1 或a=a-1之类的指令,有很多C 编译器都会生成2~3个字节的指令。

4、减少运算的强度
可以使用运算量小但功能相同的表达式替换原来复杂的的表达式。如下:

(1) 求余运算

a = a % 8;

可以改为:

a = a & 7;

说明:位操作只需一个指令周期即可完成,而大部分的C 编译器的“%”运算均是调用子程序来完成,代码长、执行速度慢。通常,只要求是求2n 方的余数,均可使用位操作的方法来代替。

(2) 平方运算

a = pow(a, 2.0);

可以改为:

a = a * a;

说明:在有内置硬件乘法器的单片机中(如51 系列),乘法运算比求平方运算快得多,因为浮点数的求平方是通过调用子程序来实现的,在自带硬件乘法器的AVR 单片机中,如ATMega163 中,乘法运算只需2 个时钟周期就可以完成。既使是在没有内置硬件乘法器的AVR单片机中,乘法运算的子程序比平方运算的子程序代码短,执行速度快。如果是求3 次方,如:

a = pow(a, 3.0);

更改为:

a = a * a * a;
则效率的改善更明显。

(3) 用移位实现乘除法运算

a = a*4;
b = b/4;

可以改为:

a = a << 2;
b = b >> 2;

说明:通常如果需要乘以或除以2n,都可以用移位的方法代替。在ICCAVR 中,如果乘以2n,都可以生成左移的代码,而乘以其它的整数或除以任何数,均调用乘除法子程序。用移位的方法得到代码比调用乘除法子程序生成的代码效率高。实际上,只要是乘以或除以一个整数,均可以用移位的方法得到结果,如:

a = a * 9

可以改为:

a = (a << 3) + a;

 

5、循环
(1) 循环语
对于一些不需要循环变量参加运算的任务可以把它们放到循环外面,这里的任务包括表达式、函数的调用、指针运算、数组访问等,应该将没有必要执行多次的操作全部集合在一起,放到一个init 的初始化程序中进行。
(2) 延时函数

通常使用的延时函数均采用自加的形式:

void delay (void)
{
    unsigned int i;
    for (i=0; i<1000; i++); 
}

将其改为自减延时函数:

void delay (void)
{
    unsigned int i;
    for (i=1000; i>0; i--); 
}

两个函数的延时效果相似,但几乎所有的C 编译对后一种函数生成的代码均比前一种代码少1~3 个字节,因为几乎所有的MCU 均有为0转移的指令,采用后一种方式能够生成这类指令。在使用while 循环时也一样,使用自减指令控制循环会比使用自加指令控制循环生成的代码更少1~3 个字母。

但是在循环中有通过循环变量“i”读写数组的指令时,使用预减循环时有可能使数组超界,要引起注意。
(3) while 循环和do…while 循环

用while 循环时有以下两种循环形式:

unsigned int i;

i = 0;

while (i<1000)
{
    i++; 
    //用户程序
}

或:

unsigned int i;
i = 1000;
do
{
    i--; 
    //用户程序
}
while (i>0);

在这两种循环中,使用do…while循环编译后生成的代码的长度短于while循环。

6、查表

在程序中一般不进行非常复杂的运算,如浮点数的乘除及开方等,以及一些复杂的数学模型的插补运算,对这些即消耗时间又消费资源的运算,应尽量使用查表的方式,并且将数据表置于程序存储区。如果直接生成所需的表比较困难,也尽量在启动时先计算,然后在数据存储器中生成所需的表,后以在程序运行直接查表就可以了,减少了程序执行过程中重复计算的工作量。

 

EOF

转自:https://mp.weixin.qq.com/s/2gjcRjxYdxnPZacZ78YZ9A

C++最佳实践 | 1. 工具

本系列是开源书C++ Best Practises[1]的中文版,全书从工具、代码风格、安全性、可维护性、可移植性、多线程、性能、正确性等角度全面介绍了现代C++项目的最佳实践。本文是该系列的第一篇。

 

C++最佳实践:

1. 工具(本文)

2. 代码风格

3. 安全性

4. 可维护性

5. 可移植性及多线程

6. 性能

7. 正确性和脚本

前言

C++最佳实践: 支持Fork的编码标准文档

本文档旨在收集对C++最佳实践所进行的协作性讨论,是《Effective C++》(Meyers)《C++ Coding Standards》(Alexandrescu, Sutter) 等书籍的补充。在讨论如何确保整体代码质量的同时,补充了一些没有讨论到的较低级别的细节,并提供了具体的风格建议。

在任何情况下,简单明了都是首选。本文所举示例是为了说明为什么一种选择比另一种更受欢迎。在必要情况下,也会用文字说明。

本文档由Jason Turner编写,根据知识共享署名-非商业4.0国际许可协议[2]授权。

免责声明

本文档的编写基于个人经验,你不需要完全同意其中的观点。本文档保存于GitHub[3]上,任何人都可以fork供自己使用,或者提交修改建议与大家分享。

本文档启发O’Reilly发布了视频: Learning C++ Best Practices[4]

工具

应该在开发过程的早期建立用于执行这些工具的自动化框架,检出源代码、构建和执行测试所使用的命令不应超过2-3个,一旦测试完成,应该对代码的状态和质量有接近完整的了解。

源码管理

对于任何软件开发项目来说,源码管理都是绝对必要的,如果还没有,那就开始使用。

  • GitHub[5] —— 允许无限制的公共存储库和私有存储库,支持最多3个协作者。
  • Bitbucket[6] —— 允许无限制的私人存储库,最多5个协作者,免费。
  • SourceForge[7] —— 仅支持托管开放源码。
  • GitLab[8] —— 免费提供无限的公共和私有存储库,包括无限的CI执行器(CI Runner)。
  • Visual Studio Online[9] (http://www.visualstudio.com/what-is-visual-studio-online-vs) —— 无限的公共存储库,私有存储库收费,支持git或TFVC。另外提供: 问题跟踪、项目计划(包括Scrum等多个敏捷模板)、集成托管构建,所有特性都可以集成到Microsoft Visual Studio中,仅支持Windows。

构建工具

使用广泛接受的行业标准构建工具,可以防止在做探索、链接新库、打包产品等等工作时重复发明轮子。例子包括:

  • CMake[10]
    • 对于构建性能,请考虑: https://github.com/sakra/cotire
    • 对于增强可用性,请考虑: https://github.com/toeb/cmakepp
    • 使用 https://cmake.org/cmake/help/v3.6/command/target_compile_features.html 作为C++标准flag
    • 考虑使用 https://github.com/cheshirekow/cmake_format 自动格式化CMakeLists.txt文件
    • CMake特定最佳实践请参考后续的延伸阅读[11]部分
    • cmake --build提供了平台无关的通用编译接口
  • Waf[12]
  • FASTBuild[13]
  • Ninja[14] —— 可以极大优化大型项目的增量构建时间,可以作为CMake的target。
  • Bazel[15] —— 基于网络工件缓存和远程执行的快速增量构建
  • Buck[16] —— 类似于Bazel,对iOS和Android有很好的支持
  • gyp[17] —— 谷歌chromium的构建工具
  • maiken[18] —— 具有maven配置风格的跨平台构建工具
  • Qt Build Suite[19] —— 基于Qt的跨平台构建工具
  • meson[20] —— 快速、对用户友好的开源构建系统
  • premake[21]

请记住,这不仅是构建工具,也是编程语言。请尽量维护良好整洁的构建脚本,并遵循正在使用的工具的推荐实践。

包管理器

包管理是C++的重要主题,目前还没有明确的赢家。请考虑使用包管理器来帮助跟踪项目的依赖关系,从而帮助新人更容易开始参与项目。

  • Conan[22] —— 跨平台C++依赖管理器
  • hunter[23] —— CMake驱动的跨平台包管理器,适用于C/C++
  • [C++ Archive Network (CPPAN)](https://cppan.org/ “C++ Archive Network (CPPAN “C++ Archive Network (CPPAN)”)”) —— 跨平台C++依赖管理器
  • qpm[24] —— Qt的包管理器
  • build2[25] —— 类Cargo的C++包管理器
  • Buckaroo[26] —— 真正去中心化的跨平台依赖管理器,适用于C/C++等等
  • Vcpkg[27] —— 微软C++库管理器,支持Windows, Linux和MacOS

持续集成

选择了构建工具之后,接下来需要设置持续集成环境。

在更改被推送到存储库时会触发持续集成(CI)工具自动构建源代码,可以私有部署CI工具或使用托管的CI系统。

  • Travis CI[28]
    • 能很好的与C++一起工作
    • 设计与GitHub一起使用
    • GitHub公共存储库可以免费使用
  • AppVeyor[29]
    • 支持Windows、MSVC和MinGW
    • GitHub公共存储库可以免费使用
  • Hudson CI[30] / Jenkins CI[31]
    • 需要Java应用服务器
    • 支持Windows、OS X和Linux
    • 可以通过许多插件进行扩展
  • TeamCity[32]
    • 对开源项目免费
  • Decent CI[33]
    • 简单持续集成,可以将结果发布到GitHub
    • 支持Windows、OS X和Linux
    • 使用ChaiScript[34]
  • Visual Studio Online[35] (http://www.visualstudio.com/what-is-visual-studio-online-vs)
    • 与Visual Studio Online的源代码库紧密集成
    • 使用MSBuild (Visual Studio的构建引擎),可在Windows、OS X和Linux上使用
    • 提供托管的构建代理,也允许用户提供构建代理
    • 可以在Microsoft Visual Studio中控制和监控
    • 通过Microsoft Team Foundation Server进行内部安装
  • GitLab[36]
    • 使用自定义Docker镜像,因此可用于C++
    • 有免费的共享执行器
    • 提供简单的覆盖率结果分析

如果在GitHub上有开源、公开托管的项目:

  • 现在就把Travis Ci和AppVeyor整合起来。关于如何在基于C++ cmake的应用程序中启用的简单示例,请参考: https://github.com/ChaiScript/ChaiScript/blob/master/.travis.yml
  • 启用覆盖工具(Codecov或Coveralls)
  • 启用Coverity Scan[37]

这些工具都是免费的,设置起来也相对容易。一旦把它们都设置好,就可以对项目进行持续的构建、测试、分析和报告,并且免费。

编译器

启用所有可用、合理的告警选项,有些告警选项只在启用了优化的情况下才有效,或者优化级别越高,效果越好,例如GCC中的-Wnull-dereference

应该使用尽可能多的编译器,每个编译器对标准的实现略有不同,支持多个编译器将有助于确保实现最可移植、最可靠的代码。

GCC / Clang

-Wall -Wextra -Wshadow -Wnon-virtual-dtor -pedantic

  • -Wall -Wextra 合理、标准
  • -Wshadow 如果变量声明覆盖了父上下文中的变量,则警告用户
  • -Wnon-virtual-dtor 如果带有虚函数的类有非虚析构函数,则警告用户,有助于捕获难以跟踪的内存错误
  • -Wold-style-cast 对C风格的类型转换发出警告
  • -Wcast-align 警告有潜在性能问题的强制类型转换
  • -Wunused 警告任何未使用的东西
  • -Woverloaded-virtual 如果重载(而不是覆盖)虚函数,则发出警告
  • -Wpedantic 如果使用了非标准的C++则发出警告(所有版本的GCC, Clang >= 3.2)
  • -Wconversion 对可能丢失数据的类型转换发出警告
  • -Wsign-conversion 对影响到符号的类型转换发出警告(Clang所有版本,GCC >= 4.3)
  • -Wmisleading-indentation 如果代码中有缩进,但没有对应的代码块,则发出警告(仅在GCC >= 6.0中)
  • -Wduplicated-cond 如果if/else分支有重复条件,则发出警告(仅在GCC >= 6.0中)
  • -Wduplicated-branches 如果if/else分支有重复的代码,则发出警告(仅在GCC >= 7.0中)
  • -Wlogical-op 在可能需要按位操作的地方使用逻辑操作时发出警告(仅在GCC中)
  • -Wnull-dereference 如果检测到空解引用将发出警告(仅在GCC >= 6.0中)
  • -Wuseless-cast 如果执行强制转换到相同的类型,则会发出警告(仅在GCC >= 4.8中)
  • -Wdouble-promotion 如果float隐式提升为double则发出警告(GCC >= 4.6, Clang >= 3.8)
  • -Wformat=2 对输出格式化函数(即printf)的安全问题发出警告
  • -Wlifetime 显示对象生命周期问题(目前只有Clang的特殊分支)

考虑使用-Weverything,并且只在需要的情况下禁用少数警告。

-Weffc++警告模式可能太吵了,但如果对项目适用,也可以使用。

MSVC

/permissive- —— 执行标准一致性[38]

/W4 /w14640 —— 使用并考虑以下内容(参见下面的描述)

  • /W4 一切合理的警告
  • /w14242 ‘identifier’: 从’type1’到’type1’的转换,可能丢失数据
  • /w14254 ‘operator’: 从“type1:field_bits”到“type2:field_bits”的转换,可能丢失数据
  • /w14263 ‘function’: 成员函数不重写任何基类虚成员函数
  • /w14265 ‘classname’: 类有虚函数,但析构函数不是该类的虚实例,可能无法正确析构
  • /w14287 ‘operator’: 无符号/负常数不匹配
  • /we4289 nonstandard extension used: ‘variable’: 在for循环中声明的循环控制变量在for循环作用域之外使用
  • /w14296 ‘operator’: 表达式总是’布尔值(boolean_value)’
  • /w14311 ‘variable’: 指针从’type1’转换到’type2’时被截断
  • /w14545 逗号前的表达式计算的是缺少参数列表的函数
  • /w14546 逗号前的函数调用缺少参数列表
  • /w14547 ‘operator’: 逗号前的运算符无效,预期运算符有副作用
  • /w14549 ‘operator’: 逗号前的运算符无效,想要“运算符”吗?
  • /w14555 表达式没有效果,表达式预期带有副作用
  • /w14619 pragma warning: 没有警告号码
  • /w14640 在线程不安全的静态成员初始化时启用警告
  • /w14826 从’type1’到’type_2’的转换会扩展符号,可能会导致意外的运行时行为
  • /w14905 宽字符串字面量转换为’LPSTR’
  • /w14906 字符串字面量转换为’LPWSTR’
  • /w14928 非法的拷贝初始化,已隐式应用多个用户定义转换

不建议

  • /Wall 会对标准库中包含的文件发出警告,有太多额外的警告,因此没什么用。
通用

一开始就设置非常严格的警告,在项目开始后试图提高警告级别可能会很痛苦。

考虑使用将警告视为错误的设置,例如MSVC中的/Wx,以及GCC/Clang中的-Werror

基于LLVM的工具

基于LLVM的工具与能够输出编译命令数据库的构建系统(例如cmake)配合得最好,例如:

$ cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON .

如果没用这样的构建系统,可以考虑Build EAR[39],它可以与现有构建系统挂钩,并生成编译命令数据库。

CMake现在也提供了在正常编译期间调用“`clang-tidy“`[40]的内置支持。

  • include-what-you-use[41], 示例结果[42]
  • clang-modernize[43], 示例结果[44]
  • clang-check[45]
  • clang-tidy[46]

静态检查

最好的选择是将静态分析器作为自动化构建系统的一部分运行,cppcheck和clang可以满足免费选项的要求。

Coverity Scan

Coverity[47]提供免费(开源)静态分析工具包,可以用于与Travis CI[48]AppVeyor[49]集成的每个提交。

PVS-Studio

PVS-Studio[50]是用于检测用C、C++和C#编写的程序源代码中的bug的工具,对个人学术项目、开源非商业项目和个人开发者的独立项目都是免费的,可以在Windows和Linux环境下工作。

Cppcheck

Cppcheck[51]是免费、开源的。它努力争取零误报,并且做得很好。因此,应该启用所有警告: --enable=all

备注:

  • 为了正确工作,需要格式完整的头文件路径,所以在使用前不要忘记传递: --check-config
  • 查找未使用的头文件时-j不能大于1。
  • 如果需要检查所有的代码,请记住为带有大量#ifdef的代码添加--force
cppclean

cppclean[52]是开源静态分析器,专注于发现C++源代码中导致大型代码库开发缓慢的问题。

CppDepend

CppDepend[53]通过分析和可视化代码依赖关系、定义设计规则、进行影响分析以及比较不同版本的代码,简化了对复杂C/C++代码库的管理,对开源贡献者是免费的。

Clang的静态分析器

Clang的分析程序的默认选项适用于各个平台,可以直接通过CMake使用[54],也可以通过基于llvm的工具[55]中的clang-checkclang-tidy调用。

此外,CodeChecker[56]可以作为clang的静态分析前端。

clang-tidy可以通过Clang Power Tools[57]扩展轻松的和Visual Studio一起使用。

MSVC的静态分析器

可以通过/analyze命令行选项[58]启用,可以使用默认选项。

Flint / Flint++

Flint[59]Flint++[60]是根据Facebook编码标准分析C++代码的linter。

OCLint

OCLint[61]是免费、自由、开源的静态代码分析工具,可以通过许多不同的方式提高C++代码的质量。

ReSharper C++ / CLion

这两种来自JetBrains[62]的工具都提供了一定程度的静态分析和自动修复功能,为开源项目负责人提供了免费许可证选项。

Cevelop

基于Eclipse的Cevelop[63] IDE提供了各种静态分析和重构/代码修复工具。例如,可以用C++的constexprs替换宏,重构命名空间(提取/内联using,限定名称),并将代码重构为C++11的统一初始化语法。Cevelop是免费的。

Qt Creator

Qt Creator可以插入clang静态分析器。

clazy

clazy[64]是基于clang的分析Qt使用情况的工具。

IKOS

IKOS[65]是开源静态分析器,由NASA开发。它以抽象解释为基础,用C++编写,使用LLVM为C和C++提供了分析器。源代码可以在Github[66]上找到。

运行时检查

代码覆盖率分析

覆盖率分析工具应该在测试执行时运行,以确保整个应用程序都被测到。不幸的是,覆盖率分析需要禁用编译器优化,这将导致测试执行时间大大延长。

  • Codecov[67]
    • 与Travis CI和AppVeyor集成
    • 对于开源项目免费
  • Coveralls[68]
    • 与Travis CI和AppVeyor集成
    • 对于开源项目免费
  • LCOV[69]
    • 有很多配置项
  • Gcovr[70]
  • kcov[71]
    • 可与codecov和coveralls集成
    • 不需要特殊的编译器flag,只需要debug符号,就可以输出代码覆盖率报告
  • OpenCppCoverage[72]
    • Windows上的开源代码覆盖率工具
Valgrind

Valgrind[73]是运行时代码分析器,可以检测内存泄漏、竞争条件和其他相关问题,支持各种Unix平台。

Dr Memory

和Valgrind类似。http://www.drmemory.org

GCC / Clang Sanitizers

这些工具提供了许多与Valgrind相同的特性,但内置在编译器中,易于使用,并提供问题报告。

  • AddressSanitizer
  • MemorySanitizer
  • ThreadSanitizer
  • UndefinedBehaviorSanitizer

注意可用的sanitizer选项,包括运行时选项。https://kristerw.blogspot.com/2018/06/useful-gcc-address-sanitizer-checks-not.html

Fuzzy分析器

如果项目接受用户定义的输入,可以考虑运行模糊输入测试。

这些工具都使用覆盖率报告来寻找新的代码执行路径,并尝试为代码提供新的输入。它们可以发现崩溃、挂起以及一些没有被考虑到的输入。

  • american fuzzy lop[74]
  • LibFuzzer[75]
  • KLEE[76] —— 可以为单独的函数提供模糊测试
变异测试

这些工具获取在单元测试运行期间执行的代码,并改变执行的代码。如果测试在有突变的情况下仍然通过,那可能意味着在测试套件中存在有缺陷的测试。

  • Dextool Mutate[77]
  • MuCPP[78]
  • mull[79]
  • CCMutator[80]
控制流保护

MSVC的[控制流保护(Control Flow Guard)](https://msdn.microsoft.com/en-us/library/windows/desktop/mt637065%28v=vs.85%29.aspx?f=255&MSPPError=-2147217396 “控制流保护(Control Flow Guard “控制流保护(Control Flow Guard)”)”)增加了高性能的运行时安全检查。

检查STL实现
  • _GLIBCXX_DEBUG与GCC的libstdc++的实现。参见Krister的博客文章[81]
堆分析
  • https://epfl-vlsc.github.io/memoro —— 一个详细的堆分析器
忽略警告

如果团队一致认为编译器或分析器对不正确或不可避免的错误发出警告,则团队需要尽可能只在最小的范围内禁用特定的错误警告。

在对一段代码禁用该警告后,请确保重新启用该警告,没人希望禁用的警告被泄露到其他代码中[82]

测试

上面提到的CMake有一个用于执行测试的内置框架,请确保使用的任何构建系统都能够执行内置测试。

为了进一步帮助执行测试,请考虑使用某个单元测试库,如Google Test[83]Catch[84]CppUTest[85]Boost.Test[86],以帮助组织测试。

单元测试

单元测试针对的是可以独立测试的小代码块和独立功能。

集成测试

对于提交的每个特性或bug修复,都应该启用测试。参见上文介绍的代码覆盖率分析。这些测试比单元测试级别更高,但仍然应该被限制在单个特性的范围内。

逆向测试

不要忘记确保测试代码中的错误处理,并且确保其能够正常工作。如果目标是100%的代码覆盖率,很明显这些错误场景也需要被覆盖的。

调试

uftrace

uftrace[87]可以用来生成程序执行的函数调用图。

rr

rr[88]是一个免费、开源的反向调试器,支持C++。

其他工具

Lizard

Lizard[89]提供了针对C++代码库运行复杂性分析的非常简单的接口。

Metrix++

Metrix++[90]可以识别并报告代码中最复杂的部分,从而帮助我们减少复杂代码,帮助编译器更好的理解和优化代码。

ABI Compliance Checker

ABI Compliance Checker[91] (ACC)可以分析两个库版本,并生成关于API和C++ ABI变化的详细兼容性报告,可以帮助库开发人员发现无意的破坏性更改,以确保向后兼容性。

CNCC

Customizable Naming Convention Checker[92](可自定义的命名约定检查器)可以报告代码中不遵循特定命名约定的标识符。

ClangFormat

ClangFormat[93]可以自动检查并纠正代码格式,以匹配组织约定。可以参考关于clang-format的系列文章[94]

SourceMeter

SourceMeter[95]提供了免费版本,可以为代码提供许多不同的度量,也可以调用cppcheck。

Bloaty McBloatface

Bloaty McBloatface[96]是用于类unix平台的二进制大小分析器。

微信公众号:DeepNoMind

参考资料

[1]

 

C++ Best Practises: https://lefticus.gitbooks.io/cpp-best-practices/content/

[2]

知识共享署名-非商业4.0国际许可协议: http://creativecommons.org/licenses/by-nc/4.0/

[3]

GitHub: https://github.com/lefticus/cppbestpractices

[4]

Learning C++ Best Practices: http://shop.oreilly.com/product/0636920049814.do

[5]

GitHub: https://github.com/

[6]

Bitbucket: https://bitbucket.org/

[7]

SourceForge: http://sourceforge.net/

[8]

GitLab: https://gitlab.com/

[9]

Visual Studio Online: https://visualstudio.com/

[10]

CMake: http://www.cmake.org/

[11]

延伸阅读: https://lefticus.gitbooks.io/cpp-best-practices/content/10-Further_Reading.md

[12]

Waf: https://waf.io/

[13]

FASTBuild: http://www.fastbuild.org/

[14]

Ninja: https://ninja-build.org/

[15]

Bazel: http://bazel.io/

[16]

Buck: http://buckbuild.com/

[17]

gyp: https://chromium.googlesource.com/external/gyp/

[18]

maiken: https://github.com/Dekken/maiken

[19]

Qt Build Suite: http://doc.qt.io/qbs/

[20]

meson: http://mesonbuild.com/index.html

[21]

premake: https://premake.github.io/

[22]

Conan: https://www.conan.io/

[23]

hunter: https://github.com/ruslo/hunter

[24]

qpm: https://www.qpm.io/

[25]

build2: https://build2.org/

[26]

Buckaroo: https://buckaroo.pm/

[27]

Vcpkg: https://github.com/microsoft/vcpkg

[28]

Travis CI: http://travis-ci.org/

[29]

AppVeyor: http://www.appveyor.com/

[30]

Hudson CI: http://hudson-ci.org/

[31]

Jenkins CI: https://jenkins-ci.org/

[32]

TeamCity: https://www.jetbrains.com/teamcity

[33]

Decent CI: https://github.com/lefticus/decent_ci

[34]

ChaiScript: http://chaiscript.com/ChaiScript-BuildResults/full_dashboard.html

[35]

Visual Studio Online: https://visualstudio.com/

[36]

GitLab: https://gitlab.com/

[37]

Coverity Scan: https://scan.coverity.com/

[38]

执行标准一致性: https://docs.microsoft.com/en-us/cpp/build/reference/permissive-standards-conformance

[39]

Build EAR: https://github.com/rizsotto/Bear

[40]

正常编译期间调用clang-tidy: https://cmake.org/cmake/help/latest/prop_tgt/LANG_CLANG_TIDY.html

[41]

include-what-you-use: https://github.com/include-what-you-use

[42]

示例结果: https://github.com/ChaiScript/ChaiScript/commit/c0bf6ee99dac14a19530179874f6c95255fde173

[43]

clang-modernize: http://clang.llvm.org/extra/clang-modernize.html

[44]

示例结果: https://github.com/ChaiScript/ChaiScript/commit/6eab8ddfe154a4ebbe956a5165b390ee700fae1b

[45]

clang-check: http://clang.llvm.org/docs/ClangCheck.html

[46]

clang-tidy: http://clang.llvm.org/extra/clang-tidy.html

[47]

Coverity: https://scan.coverity.com/

[48]

Travis CI: http://travis-ci.org/

[49]

AppVeyor: http://www.appveyor.com/

[50]

PVS-Studio: http://www.viva64.com/en/pvs-studio/

[51]

Cppcheck: http://cppcheck.sourceforge.net/

[52]

cppclean: https://github.com/myint/cppclean

[53]

CppDepend: https://www.cppdepend.com/

[54]

通过CMake使用: http://garykramlich.blogspot.com/2011/10/using-scan-build-from-clang-with-cmake.html

[55]

基于llvm的工具: https://lefticus.gitbooks.io/cpp-best-practices/content/02-Use_the_Tools_Available.html#llvm-based-tools

[56]

CodeChecker: https://github.com/Ericsson/CodeChecker

[57]

Clang Power Tools: https://clangpowertools.com/

[58]

命令行选项: http://msdn.microsoft.com/en-us/library/ms173498.aspx

[59]

Flint: https://github.com/facebook/flint

[60]

Flint++: https://github.com/L2Program/FlintPlusPlus

[61]

OCLint: http://oclint.org/

[62]

JetBrains: https://www.jetbrains.com/cpp/

[63]

Cevelop: https://www.cevelop.com/

[64]

clazy: https://github.com/KDE/clazy

[65]

IKOS: https://ti.arc.nasa.gov/opensource/ikos/

[66]

Github: https://github.com/NASA-SW-VnV/ikos

[67]

Codecov: https://codecov.io/

[68]

Coveralls: https://coveralls.io/

[69]

LCOV: http://ltp.sourceforge.net/coverage/lcov.php

[70]

Gcovr: http://gcovr.com/

[71]

kcov: http://simonkagstrom.github.io/kcov/index.html

[72]

OpenCppCoverage: https://github.com/OpenCppCoverage/OpenCppCoverage

[73]

Valgrind: http://www.valgrind.org/

[74]

american fuzzy lop: http://lcamtuf.coredump.cx/afl/

[75]

LibFuzzer: http://llvm.org/docs/LibFuzzer.html

[76]

KLEE: http://klee.github.io/

[77]

Dextool Mutate: https://github.com/joakim-brannstrom/dextool/tree/master/plugin/mutate

[78]

MuCPP: https://neptuno.uca.es/redmine/projects/mucpp-mutation-tool/wiki

[79]

mull: https://github.com/mull-project/mull

[80]

CCMutator: https://github.com/markus-kusano/CCMutator

[81]

Krister的博客文章: https://kristerw.blogspot.se/2018/03/detecting-incorrect-c-stl-usage.html

[82]

泄露到其他代码中: http://www.forwardscattering.org/post/48

[83]

Google Test: https://github.com/google/googletest

[84]

Catch: https://github.com/philsquared/Catch

[85]

CppUTest: https://github.com/cpputest/cpputest

[86]

Boost.Test: http://www.boost.org/doc/libs/release/libs/test/

[87]

uftrace: https://github.com/namhyung/uftrace

[88]

rr: http://rr-project.org/

[89]

Lizard: http://www.lizard.ws/

[90]

Metrix++: http://metrixplusplus.sourceforge.net/

[91]

ABI Compliance Checker: http://ispras.linuxbase.org/index.php/ABI_compliance_checker

[92]

Customizable Naming Convention Checker: https://github.com/mapbox/cncc

[93]

ClangFormat: http://clang.llvm.org/docs/ClangFormat.html

[94]

关于clang-format的系列文章: https://engineering.mongodb.com/post/succeeding-with-clangformat-part-1-pitfalls-and-planning/

[95]

SourceMeter: https://www.sourcemeter.com/

[96]

Bloaty McBloatface: https://github.com/google/bloaty

 

– END –

转自:https://mp.weixin.qq.com/s/_ZC1t0wMu8fjTq-1HgKrrA

现代C++学习指南-标准库

在上一章我们探讨了C++的类型系统,并提出了从低到高,又从高到低的学习思路,本文就是一篇从高到低的学习指南,希望能提供一种新的视角。

什么是标准库

编程语言一般分为两个部分,一部分是语法部分,如上一章的类型系统,另一部分则是用这套语法完成的预定义的工具集,如本文的主题——标准库。标准库是一堆我们写代码时直接可以用的代码,就像是我们提前写好的一样,不仅如此,标准库还是跨平台的,还是经过工业级测试的,所以标准库有着靠谱,安全的特点。
C++标准库包括很多方面,有类vectorstring等,有对象std::cinstd::cout等,还有函数movecopy等,所以一般按功能来对它们分类

  • 容器类

  • 算法类

  • 智能指针

  • 线程相关

  • 其他

当然,这些还不是全部,标准库是在不断扩充和完善的,学习标准库的宗旨也应该是学习它们的使用场景,而不是深入用法。比如容器类中就有很多功能类似的类,不同的业务场景有不同的选择。通过对它们的了解,我们更容易写出高效,简洁的代码。

容器类

容器类就是帮助管理一组数据的类,根据实现方式的不同,分为有序列表,无序列表和映射。
有序列表中的有序是指,数据组保存在一块连续的内存区域里,可以通过插入时的索引直接定位到原数据。因为数据是按顺序存入的,所以中途假如需要删除或者新增数据,在操作位置右边的数据都需要移动,操作的代价就比较大。由此也可看出它们的优势是顺序插入和尾部修改,还有直接查找,这方面的代表就是arrayvector
array是对原始数组的封装,并且解决了传递数组变成指针这样的问题,但是缺点是它的大小是固定的,适合用在数据量已知的情况。而vector又是对array的增强,不仅能完成所有array的操作,并且大小可变,所以绝大部分情况下,选择vector都是理想的选择。

现代C++学习指南-标准库


无序列表的元素是单独存储的,相互之间用指针来查找相邻元素,由于指针可以轻易修改指向的指,所以对相邻元素的修改就变得很快捷。同样的道理,查找相邻元素只能靠指针跳转,查找某个值需要从一个指针开始查找,一次跳转一条数据,直到找到目标或者没有数据为止。所以无序列表的优势是快速地删除和插入新数据,不适合查找,其代表有listforward_list。显然,有序列表和无序列表是互补的,我们在实际项目中,应该根据数据的操作来确定选择哪种容器。

现代C++学习指南-标准库

映射则融合了有序列表和无序列表的优点,既可以快速插入和删除,又可以快速查找。为了满足各种使用场景,C++提供了mapmultimapunordered_mapunordered_multimap。从名字上就能看出来它们的差别。为了直观,我直接列了一个表

  是否排序 是否支持相同值 速度
unordered_map ❤️❤️❤️❤️
map ❤️❤️
multimap ❤️
unordered_multimap ❤️❤️❤️

映射存储的是两个值,不同的类型实现方式不一样。由于map是需要排序的,所以通常它的实现是一种平衡二叉树,键就是它排序的依据。

现代C++学习指南-标准库


unordered_map是不需要排序的,所以它的实现通常是哈希表,即根据哈希函数的确定索引位置继而确定存储位置。

现代C++学习指南-标准库


综上,容器类提供了一种操作多个同类型数据的接口,开发者通过对容器类方法的调用,可以实现对容器内数据的增删改查。大部分情况下,vector都是靠谱的选择,它提供了全功能的数据操作接口,支持动态长度,索引查询,并且简单高效。如果需要频繁地插入或者删除操作,也可以考虑list或者forward_listmap可以让数据保持有序,需要更快的速度而不是排序的话unorderer_map是更好的选择,如果相同值会出现多次就可以使用对应的multi版本。另外容器类也是很好的数据结构学习资源,C++的容器类几乎提供了数据结构中所有的形式,对数据结构越熟悉选择的容器类就越完美。

 

算法

之所以将算法放在容器类后面,是因为算法大部分是对容器类操作的加强,算法都定义在algorithm文件头里。这些算法都是短小精悍的,可以大大增加代码可读性,并且妥善处理了很多容易遗忘的边界问题。功能上可以分为增删改查几种操作,可以在实际有需要的时候在查看文档,具体可以参阅这里

智能指针

很早以前,我对智能指针的态度不是很好。因为刚开始学习C++时我就知道,不能单独使用指针,要把指针封装在类里,利用类的构造函数和析构函数管理指针,也就是RAII。最开始我以为这就够了,直到我遇到下面这种情况

 1public:
2    Ptr():p{ new int } {}
3    ~Ptr() {
4        delete p;
5    }
6    intget() {
7        return *p;
8    }
9
10    void set(const int value) {
11        *p = value;
12    }
13private:
14    int* p;
15};
16
17void use(Ptr p) {
18    //传进来的是复制构造出来的p',函数返回后p'被销毁啦,两个指针指向的地址被回收,外面的p指针成为了野指针
19}
20int main() {
21    Ptr p;
22    p.set(1);
23    use(p); //p按值传递,调用了Ptr的复制构造函数,构造出了新对象p',它的指针和p的指针指向同一个地方
24    std::cout << p.get() << std::endl//p已经被销毁了,访问p的地址非法
25    return 0;
26}

调用use时,变量p被拷贝,也就出现了两个指针同时指向一块内存地址的情况。use函数执行完后,它的参数p被回收。也就是调用了Ptr的析构函数,也就是两个指针指向的地址被回收。所以24行调用get读取那个已经被回收了的地址就是非法操作,程序崩溃。
这可能是新手比较常遇到的一个问题,当然,解决这个问题也很简单,还用不到智能指针,只需要将函数use的参数改为引用类型就可以了,因为引用只是别名,不会产生新的指针,这也是我在类型系统篇中极力推荐引用为首选参数类型的原因之一。对于此例,数据不大,直接重写复制构造函数,重新申请一块内存也是一种思路。
此例中用到Ptr的地方只有一个,实际项目中Ptr往往需要用到很多次,我们不能保证不会出现忘记使用引用类型的情况,这种情况下重新申请内存也不适用,所以这个时候就需要智能指针来帮忙了。
现在思考另一种情况,某些操作我们不得不暴露出我们的指针供外部使用,随着业务的嵌套和调用链增加,很多时候会忘记或者不确定在什么时候调用delete释放内存。这也是用智能指针的一个场景。以上两种情况都是需要分享指针,对应智能指针中的shared_ptr
shared_ptr顾名思义,它可以帮助开发者完成指针共享的问题,并且完美解决提前释放,不知何时释放,谁负责释放的问题。它的对应关系是一对多,一个实际的内存可以被多个shared_ptr共享

现代C++学习指南-标准库


另外一种场景是我们希望自始至终某个指针某个时刻只属于一个对象,外部想要使用它要么通过拥有该指针的对象方法,要么把指针的所有权转移到自己身上,这种场景对应智能指针中的unique_ptr

现代C++学习指南-标准库


unique_ptr的对应关系是一对一,无论哪个时刻,只能有一个管理者拥有指针,也就只能由它负责释放了。假如想转移这种对应关系,只能通过std::move操作,不过这个操作之后,原先对象的指针就失效了,它也不再负责管理,所有的任务移交给了新的对象。这种特性特别适合资源敏感型的应用。

 

线程库

除了内存,线程是开发中另一个重要的课题。线程的难点在于不仅要管理线程对象,还要管理线程对象管理的资源,并且保证线程间数据同步。当然标准库已经做得足够好了,我们需要理解的是使用场景的问题。线程库主要包括线程对象thread,条件对象condition_variable,锁对象mutex
使用thread可以很方便地把程序写成多线程,只需要三步:

 1void plus(int a,int b)//第一步:定义线程中要运行的函数
2    std::cout<<"running at sub thread"<<std::endl;
3    std::cout<<"a + b = "<<a+b<<std::endl;
4}
5
6int main(){
7    std::thread thread{plus,1,1}; //第二步,定义std::thread对象,将函数作为参数
8    std::cout<<"continue running at main thread"<<std::endl;
9    thread.join(); //第三步调用线程对象的join函数或者detach函数
10    std::cout<<"sub thread finished!"<<std::endl;
11}
12//输出
13//    continue running at main thread
14//    running at sub thread
15//     a + b = 2
16//     sub thread finished!

难点在线程间通信,也就是解决两个问题

  1. 线程1更新了变量v的值

  2. 线程2马上能读取到正确的变量v的值,即线程1更新的那个最新值

为了协调这两个过程,就出现了锁对象mutex和条件对象condition_variable。锁对象mutex保证变量按照正确的顺序更改。条件对象condition_variable保证更改能被其他线程监听到。

 1int a,b;
2bool ready = false;
3std::mutex mux;
4std::condition_variable con;
5
6void plus() {
7    std::cout << "running at sub thread" << std::endl;
8    //因为我们要读取ready的最新值,所以要用锁保证读取结果的有效性
9    std::unique_lock<std::mutex> guard{ mux };
10    if (!ready) {
11        //数据没准备好,休息一下!
12        con.wait(guard); 
13    }
14    //这里就可以正确读变量a,b了
15    std::cout << "a + b =" << a + b << std::endl;
16}
17
18int main() {
19    std::thread thread{ plus};
20    std::cout << "continue running at main thread" << std::endl;
21    std::cout << "input a = ";
22    std::cin >> a;
23    std::cout << "input b = ";
24    std::cin >> b;
25    {
26        //数据准备好了,该通知子线程干活了,用大括号是因为想让锁因为guard的销毁即使释放,从未保证plus里面能重新获得锁
27        std::unique_lock<std::mutex> guard{ mux };
28        //更新数据
29        ready = true;
30        //通知
31        con.notify_all();
32    }
33    thread.join();
34    std::cout << "sub thread finished!" << std::endl;
35}

多线程另一个需要注意的问题就是死锁。死锁的前提是有两个锁

  1. 线程1得到了锁a,还想得锁b

  2. 线程2得到了锁b,还想得锁a

然后,再加上一个前提:某一时刻,只有一个线程能拥有某个锁,就不难得出以下结论:线程a,b除非某一个放弃已得的锁,不然两个线程都会因为没得到需要的锁而一直死等,形成死锁。同时解决死锁的思路也呼之欲出:既然一个得了a,一个得了b,而锁同一时间只能被一个线程得到,那么所有线程都按先得a,再得b的顺序来就不会有锁被占用的问题了。另一个思路则可以从放弃上入手,既然都得不到,那么接下来的任务也做不了,不如直接放弃已经得到的,所以可以考虑使用timed_mutex

其他

还有很多常用的库,如字符串string,时间chrono,还有在定义函数变量时常用的functional,异常exception,更多的内容可以在cplusplus找的参考。

总结

总的来说,标准库提供了一个展现C++语言能力的平台:帮助开发者更好更快完成开发任务的同时,还能启迪开发者实现更好的抽象和实践。如我就从标准库中学到了更规范地定义函数参数,更好的封装,以及其他好的思路。学习标准库不仅更好地掌握了语言本身,还掌握了更全面地分析问题,解决问题的方法,是值得花费一段时间学习的。
容器类是几乎所有项目都会用到的,也是比较好掌握的,主要可以从数据结构方面对照学习;智能指针则是处理指针问题的好帮手;线程相关的库是比较难掌握的,关键是要想明白使用场景和极端情况下的边界问题。很多时候边界问题可能不那么直观。如线程要求获得锁的情况就分为:锁空闲,锁被其他线程占有,锁被自己占有。不同的边界对于不同的锁,预期结果也是不同的,只有在明确场景的情况下,才能更好地理清锁的关系,从而解决好问题。
最好的学习还是在实践中主动使用。对于我,通常在遇到新问题的时候会先查查标准库有没有相应的库,有的话就是学习这个库的好时机。可以先概览库的定义和解决的问题,然后分析它提供的类,函数,对象等,再将自己的理解转换为项目中的代码,最后在实际效果中检验和修正想法,完成库的学习。

转自:https://mp.weixin.qq.com/s/e61DYod_0oypXismlumHpQ

STL总结与常见面试题

1 STL概述

为了建立数据结构和算法的一套标准,并且降低他们之间的耦合关系,以提升各自的独立性、弹性、交互操作性(相互合作性,interoperability),诞生了STL。

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

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

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

  • 迭代器:扮演了容器与算法之间的胶合剂,共有五种类型,从实现角度来看,迭代器是一种将operator* , operator-> , operator++,operator–等指针相关操作予以重载的class template. 所有STL容器都附带有自己专属的迭代器,只有容器的设计者才知道如何遍历自己的元素。原生指针(native pointer)也是一种迭代器。

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

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

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

STL六大组件的交互关系,容器通过空间配置器取得数据存储空间,算法通过迭代器存储容器中的内容,仿函数可以协助算法完成不同的策略的变化,适配器可以修饰仿函数。

2 STL的优点:

  1. STL 是 C++的一部分,因此不用额外安装什么,它被内建在你的编译器之内。
  2. STL 的一个重要特性是将数据和操作分离。数据由容器类别加以管理,操作则由可定制的算法定义。迭代器在两者之间充当“粘合剂”,以使算法可以和容器交互运作
  3. 程序员可以不用思考 STL 具体的实现过程,只要能够熟练使用 STL 就 OK 了。这样他们就可以把精力放在程序开发的别的方面。
  4. STL 具有高可重用性,高性能,高移植性,跨平台的优点。
  5. 高可重用性:STL 中几乎所有的代码都采用了模板类和模版函数的方式实现,这相比于传统的由函数和类组成的库来说提供了更好的代码重用机会。
  6. 高性能:如 map 可以高效地从十万条记录里面查找出指定的记录,因为 map 是采用红黑树的变体实现的。
  7. 高移植性:如在项目 A 上用 STL 编写的模块,可以直接移植到项目 B 上。容器和算法之间通过迭代器进行无缝连接。STL 几乎所有的代码都采用了模板类或者模板函数,这相比传统的由函数和类组成的库来说提供了更好的代码重用机会。

3 容器

STL容器就是将运用最广泛的一些数据结构实现出来。

常用的数据结构:数组(array) , 链表(list), tree(树),栈(stack), 队列(queue), 集合(set),映射表(map), 根据数据在容器中的排列特性,这些数据分为序列式容器和关联式容器两种。

序列式容器强调值的排序,序列式容器中的每个元素均有固定的位置,除非用删除或插入的操作改变这个位置。Vector容器、Deque容器、List容器等。

关联式容器是非线性的树结构,更准确的说是二叉树结构。各元素之间没有严格的物理上的顺序关系,也就是说元素在容器中并没有保存元素置入容器时的逻辑顺序。关联式容器另一个显著特点是:在值中选择一个值作为关键字key,这个关键字对值起到索引的作用,方便查找。Set/multiset容器 Map/multimap容器

容器 底层数据结构 时间复杂度 有无序 可不可重复
array 数组 随机读改 O(1) 无序 可重复
vector 数组 随机读改、尾部插入、尾部删除 O(1)头部插入、头部删除 O(n) 无序 可重复
deque 双端队列 头尾插入、头尾删除 O(1) 无序 可重复
forward_list 单向链表 插入、删除 O(1) 无序 可重复
list 双向链表 插入、删除 O(1) 无序 可重复
stack deque / list 顶部插入、顶部删除 O(1) 无序 可重复
queue deque / list 尾部插入、头部删除 O(1) 无序 可重复
priority_queue vector /max-heap 插入、删除 O(log2n) 有序 可重复
set 红黑树 插入、删除、查找 O(log2n) 有序 不可重复
multiset 红黑树 插入、删除、查找 O(log2n) 有序 可重复
map 红黑树 插入、删除、查找 O(log2n) 有序 不可重复
multimap 红黑树 插入、删除、查找 O(log2n) 有序 可重复
unordered_set 哈希表 插入、删除、查找 O(1) 最差 O(n) 无序 不可重复
unordered_multiset 哈希表 插入、删除、查找 O(1) 最差 O(n) 无序 可重复
unordered_map 哈希表 插入、删除、查找 O(1) 最差 O(n) 无序 不可重复
unordered_multimap 哈希表 插入、删除、查找 O(1) 最差 O(n) 无序 可重复

1 array

array 是固定大小的顺序容器,它们保存了一个以严格的线性顺序排列的特定数量的元素。

方法 说明
begin 返回指向数组容器中第一个元素的迭代器
end 返回指向数组容器中最后一个元素之后的理论元素的迭代器
rbegin 返回指向数组容器中最后一个元素的反向迭代器
rend 返回一个反向迭代器,指向数组中第一个元素之前的理论元素
cbegin 返回指向数组容器中第一个元素的常量迭代器(const_iterator)
cend 返回指向数组容器中最后一个元素之后的理论元素的常量迭代器(const_iterator)
crbegin 返回指向数组容器中最后一个元素的常量反向迭代器(const_reverse_iterator)
crend 返回指向数组中第一个元素之前的理论元素的常量反向迭代器(const_reverse_iterator)
size 返回数组容器中元素的数量
max_size 返回数组容器可容纳的最大元素数
empty 返回一个布尔值,指示数组容器是否为空
operator[] 返回容器中第 n(参数)个位置的元素的引用
at 返回容器中第 n(参数)个位置的元素的引用
front 返回对容器中第一个元素的引用
back 返回对容器中最后一个元素的引用
data 返回指向容器中第一个元素的指针
fill 用 val(参数)填充数组所有元素
swap 通过 x(参数)的内容交换数组的内容
get(array) 形如 std::get<0>(myarray);传入一个数组容器,返回指定位置元素的引用
relational operators (array) 形如 arrayA > arrayB;依此比较数组每个元素的大小关系

测试代码

#include<iostream>
#include<array>
using namespace std;

int main() 
{
	array<int, 8> myArr = {1,3,4,6,9};//固定大小为8
	cout << "myArr元素序列:";
	for (auto i = 0; i < 8; ++i) 
	{
		cout << myArr[i] << " ";
	}
	cout << endl;

	array<int, 8> myArr1 = {2,3,4,7,8,9};//固定大小为8
	cout << "myArr1元素序列:";
	for (auto i = 0; i < 8; ++i) 
	{
		cout << myArr1[i] << " ";
	}
	cout << endl;

	myArr.swap(myArr1);   //交换两个容器的内容
	cout << "交换myArr与myArr1"<< endl;
	cout << endl;

	cout << "myArr.at(3) = " << myArr.at(3) << endl;//任意访问
	cout << "myArr[3] = " << myArr[3] << endl;//任意访问
	cout << "myArr.front() = " << myArr.front() << endl;//获取第一个元素
	cout << "myArr.back() =  " << myArr.back() << endl;//获取最后一个元素
	cout << "myArr.data() = " << myArr.data() << endl;//获取第一个元素的指针
	cout << "*myArr.data() = " << *myArr.data() << endl;//获取第一个元素的指针指向的元素

	cout << "正向迭代器遍历容器:";
	for (auto it = myArr.begin(); it != myArr.end(); ++it) 
	{
		cout << *it << " ";
	}
	cout << endl;
	//逆向迭代器测试
	cout << "逆向迭代器遍历容器:";
	for (auto rit = myArr.rbegin(); rit != myArr.rend(); ++rit) 
	{
		cout << *rit << " ";
	}
	cout << endl;
	//正向常迭代器测试
	cout << "正向常迭代器遍历容器:";
	for (auto it = myArr.cbegin(); it != myArr.cend(); ++it) 
	{
		cout << *it << " ";
	}
	cout << endl;
	//逆向常迭代器测试
	cout << "逆向常迭代器遍历容器:";
	for (auto rit = myArr.crbegin(); rit != myArr.crend(); ++rit) 
	{
		cout << *rit << " ";
	}
	cout << endl;
	if(myArr.empty())
		cout << "myArr为空 " << endl;
	else
		cout << "myArr不为空 " << endl;
	cout << "myArr.size() = " << myArr.size() << endl;
	cout << "myArr.max_size() = " << myArr.max_size() << endl;

	return 0;
}

运行结果

STL总结与常见面试题
运行结果

vector

vector 是表示可以改变大小的数组的序列容器。

方法 说明
vector 构造函数
~vector 析构函数,销毁容器对象
operator= 将新内容分配给容器,替换其当前内容,并相应地修改其大小
begin 返回指向容器中第一个元素的迭代器
end 返回指向容器中最后一个元素之后的理论元素的迭代器
rbegin 返回指向容器中最后一个元素的反向迭代器
rend 返回一个反向迭代器,指向中第一个元素之前的理论元素
cbegin 返回指向容器中第一个元素的常量迭代器(const_iterator)
cend 返回指向容器中最后一个元素之后的理论元素的常量迭代器(const_iterator)
crbegin 返回指向容器中最后一个元素的常量反向迭代器(const_reverse_iterator)
crend 返回指向容器中第一个元素之前的理论元素的常量反向迭代器(const_reverse_iterator)
size 返回容器中元素的数量
max_size 返回容器可容纳的最大元素数
resize 调整容器的大小,使其包含 n(参数)个元素
capacity 返回当前为 vector 分配的存储空间(容量)的大小
empty 返回 vector 是否为空
reserve 请求 vector 容量至少足以包含 n(参数)个元素
shrink_to_fit 要求容器减小其 capacity(容量)以适应其 size(元素数量)
operator[] 返回容器中第 n(参数)个位置的元素的引用
at 返回容器中第 n(参数)个位置的元素的引用
front 返回对容器中第一个元素的引用
back 返回对容器中最后一个元素的引用
data 返回指向容器中第一个元素的指针
assign 将新内容分配给 vector,替换其当前内容,并相应地修改其 size
push_back 在容器的最后一个元素之后添加一个新元素
pop_back 删除容器中的最后一个元素,有效地将容器 size 减少一个
insert 通过在指定位置的元素之前插入新元素来扩展该容器,通过插入元素的数量有效地增加容器大小
erase 从 vector 中删除单个元素(position)或一系列元素([first,last)),这有效地减少了被去除的元素的数量,从而破坏了容器的大小
swap 通过 x(参数)的内容交换容器的内容,x 是另一个类型相同、size 可能不同的 vector 对象
clear 从 vector 中删除所有的元素(被销毁),留下 size 为 0 的容器
emplace 通过在 position(参数)位置处插入新元素 args(参数)来扩展容器
emplace_back 在 vector 的末尾插入一个新的元素,紧跟在当前的最后一个元素之后
get_allocator 返回与vector关联的构造器对象的副本
swap(vector) 容器 x(参数)的内容与容器 y(参数)的内容交换。两个容器对象都必须是相同的类型(相同的模板参数),尽管大小可能不同
relational operators (vector) 形如 vectorA > vectorB;依此比较每个元素的大小关系

测试代码

#include <vector>
#include <iostream>
using namespace std;

int main()
{

	//构造函数,复制构造函数(元素类型要一致),
	vector<int> vecA;  //创建一个空的的容器
	vector<int> vecB(10,20); //创建一个10个元素,每个元素值为20
	vector<int> vecC(vecB.begin(),vecB.end()); //使用迭代器,可以取部分元素创建一个新的容器
	vector<int> vecD(vecC); //复制构造函数,创建一个完全一样的容器

	//重载=
	vector<int> vecE;
	vecE = vecB;

	//vector::begin(),返回的是迭代器

	vector<int> vecF(10); //创建一个有10个元素的容器
	cout << "vecF:";
	for (int i = 0; i < 10; i++)
	{
		vecF[i] = i;
		cout << vecF[i]<< " ";
	}
	cout << endl;

	//vector::begin() 返回迭代器
	vector<int>::iterator Beginit = vecF.begin();
	cout<< "vecF.begin():" << *Beginit << endl; 

	//vector::end() 返回迭代器
	vector<int>::iterator EndIter = vecF.end();
	EndIter--; //向后移一个位置
	cout <<"vecF.end():"<< *EndIter << endl; 

	//vector::rbegin() 返回倒序的第一个元素,相当于最后一个元素
	vector<int>::reverse_iterator ReverBeIter = vecF.rbegin();
	cout << "vecF.rbegin(): "<< *ReverBeIter << endl; 

	//vector::rend() 反序的最后一个元素下一个位置,也相当于正序的第一个元素前一个位置
	vector<int>::reverse_iterator ReverEnIter = vecF.rend();
	ReverEnIter--;
	cout << "vecF.rend():"<< *ReverEnIter << endl; 

	//vector::size() 返回元素的个数
	cout << "vecF.size():"<< vecF.size() << endl; 

	//vector::max_size()
	cout << "vecF.max_size():"<< vecF.max_size() << endl; 

	//vector::resize()
	cout<< "vecF.size():" << vecF.size() << endl; 
	vecF.resize(5);

	cout<< "调整vecF大小后重新赋值:"; 
	for(int k = 0; k < vecF.size(); k++)
		cout << vecF[k] << "  "; 
	cout << endl;

	//vector::capacity()
	cout<< "调整后vecF.size():"<< vecF.size() << endl; 
	cout<< "调整后vecF.capacity():" << vecF.capacity() << endl; 

	//vector::empty()
	vecB.resize(0);
	cout<< "vecB.resize(0)后"<< endl; 

	cout  << "vecB.size():" << vecB.size() << endl; 
	cout  << "vecB.capacity():" << vecB.capacity() << endl; 
	if(vecB.empty())
	    cout << "vecB为空"<< endl; 
	else
		cout << "vecB不为空"<< endl; 

	//vector::reserve() //重新分配存储空间大小
	cout<< "vecC.capacity():" << vecC.capacity() << endl; //

	vecC.reserve(4);
	cout << "vecC.reserve(4)后vecC.capacity(): "<< vecC.capacity() << endl; //10
	vecC.reserve(14);
	cout << "vecC.reserve(14)后vecC.capacity(): "<< vecC.capacity() << endl; //14

	//vector::operator []
	cout << "vecF[0]:"<< vecF[0] << endl; //第一个元素是0

	//vector::at()
	try
	{
		cout << "vecF.size = " << vecF.size() << endl; //5
		cout << vecF.at(6) << endl; //抛出异常
	}
	catch(out_of_range)
	{	
		cout << "at()访问越界" << endl;
	}

	//vector::front() 返回第一个元素的值
	cout << "vecF.front():"<< vecF.front() << endl; //0

	//vector::back()
	cout << "vecF.back():"<< vecF.back() << endl; //4

	//vector::assign()
	cout <<"vecA.size():"<< vecA.size() << endl; //0
	vector<int>::iterator First = vecC.begin();
	vector<int>::iterator End = vecC.end()-2;
	vecA.assign(First,End);
	cout << vecA.size() << endl; //8
	cout << vecA.capacity() << endl; //8

	vecA.assign(5,3); //将丢弃原来的所有元素然后重新赋值
	cout << vecA.size() << endl; //5
	cout << vecA.capacity() << endl; //8

	//vector::push_back()
	cout << *(vecF.end()-1) << endl; //4
	vecF.push_back(20);
	cout << *(vecF.end()-1) << endl; //20

	//vector::pop_back()
	cout << *(vecF.end()-1) << endl; //20
	vecF.pop_back();
	cout << *(vecF.end()-1) << endl; //4

	//vector::swap()
	cout << "vecF:";
	for (int i = 0; i < vecF.size(); i++)
	{
		vecF[i] = i;
		cout << vecF[i]<< " ";
	}
	cout << endl;
	cout << "vecD:";
	for (int d = 0; d < vecD.size(); d++)
	{
		vecD[d] = d;
		cout << vecD[d]<< " ";
	}
	cout << endl;

	vecF.swap(vecD); //交换这两个容器的内容
	cout <<"vecD与vecF交换后:" <<endl;
	cout << "vecF:";
	for(int f = 0; f < vecF.size(); f++)
		cout << vecF[f] << " ";
	cout << endl;

	cout << "vecD:";
	for (int d = 0; d <vecD.size(); d++)
		cout << vecD[d] << " ";
	cout << endl;
	//vector::clear()
	vecF.clear();
	cout << "vecF.clear()后vecF.size():"<< vecF.size() << endl;     //0
	cout << "vecF.clear()后vecF.capacity():"<< vecF.capacity() << endl; //10

	return 0;
}

运行结果

STL总结与常见面试题
运行结果

deque

deque容器为一个给定类型的元素进行线性处理,像向量一样,它能够快速地随机访问任一个元素,并且能够高效地插入和删除容器的尾部元素。但它又与vector不同,deque支持高效插入和删除容器的头部元素,因此也叫做双端队列。

STL总结与常见面试题

deque的中控器: deque是由一段一段的定量连续空间构成。一旦有必要在deque的前端或尾端增加新空间,便配置一段定量连续空间,串接在整个deque的头端或尾端。deque的最大任务,便是在这些分段的定量连续空间上,维护其整体连续的假象,并提供随机存取的接口。避开了“重新配置、复制、释放”的轮回,代价则是复杂的迭代器结构。

STL总结与常见面试题

deque采用一块所谓的map(不是STL的map容器)作为主控。

map是一小块连续空间,其中每个元素(此处称为一个节点,node)都是指针,指向另一段(较大的)连续线性空间,称为缓冲区。

缓冲区才是deque的储存空间主体。

template<class T, class Alloc = alloc, size_t BufSiz = 0>  
class deque{  
public :  
    typedef T value_type ;  
    typedef value_type* pointer ;  
    ...  
protected :  
    //元素的指针的指针(pointer of pointer of T)  
    // 其实就是T**,一个二级指针,维护一个二维数组
    typedef pointer* map_pointer ; 
  
protected :  
    map_pointer map ; //指向map,map是块连续空间,其内的每个元素  
                      //都是一个指针(称为节点),指向一块缓冲区  
    size_type map_size ;//map内可容纳多少指针  
    ...  
};  

map其实是一个T**,也就是说它是一个指针,所指之物也是一个指针,指向型别为T的一块空间。

方法 说明
deque 构造函数
push_back 在当前的最后一个元素之后 ,在 deque 容器的末尾添加一个新元素
push_front 在 deque 容器的开始位置插入一个新的元素,位于当前的第一个元素之前
pop_back 删除 deque 容器中的最后一个元素,有效地将容器大小减少一个
pop_front 删除 deque 容器中的第一个元素,有效地减小其大小
emplace_front 在 deque 的开头插入一个新的元素,就在其当前的第一个元素之前
emplace_back 在 deque 的末尾插入一个新的元素,紧跟在当前的最后一个元素之后

测试代码

#include "stdafx.h"
#include<iostream>
#include<deque>
 
using namespace std;
int main()
{
	deque<int> d;
	d.push_back( 11 );//在 deque 容器的末尾添加一个新元素
	d.push_back(20);
	d.push_back(35);
	cout<<"初始化双端队列d:"<<endl;
	for(int i = 0; i < d.size(); i++)
	{
		cout<<d.at(i)<<"t";
	}
	cout<<endl;

	d.push_front(10);//容器的开始位置插入一个新的元素,位于当前的第一个元素之前
	d.push_front(7);
	d.push_front(1);
 
	cout<<"队列d向前陆续插入10、7、1:"<<endl;
	for(int i = 0;i < d.size();i++)
	{
		cout<<d.at(i)<<"t";
	}
	cout<<endl;

	d.pop_back(); //删除 deque 容器中的最后一个元素,有效地将容器大小减少一个
	d.pop_front(); //删除 deque 容器中的第一个元素,有效地减小其大小
	cout<<"删除deque最后一个和第一个元素后:"<<endl;
	for(int i = 0;i < d.size();i++)
	{
		cout<<d.at(i)<<"t";
	}
	cout<<endl;
	return 0;
}

forward_list

在头文件<forward_list>中,与list类似,区别就是list时双链表,forward_list是单链表,forward_list(单向链表)是序列容器,允许在序列中的任何地方进行恒定的时间插入和擦除操作。在链表的任何位置进行插入/删除操作都非常快。

STL总结与常见面试题

forward_list的特点

  • forward_list只提供钱箱迭代器,因此不支持反向迭代器,比如rbegin()等成员函数。
  • forward_list不提供size()成员函数。
  • forward_list没有指向最末元素的锚点,因此不提供back()、push_back()和pop_back()。
  • forward_list不提供随机访问,这一点跟list相同。
  • 插入和删除元素不会造成“指向至其他元素”的指针,引用和迭代器失效。

容器成员函数总结就不写了,太多影响阅读,感兴趣小伙伴戳http://www.cplusplus.com/reference/stl/

list

STL总结与常见面试题

list双向链表,是序列容器,允许在序列中的任何地方进行常数时间插入和擦除操作,并在两个方向上进行迭代,可以高效地进行插入删除元素。

使用list容器之前必须加上头文件:#include;

list容器的底层实现:

和 array、vector 这些容器迭代器的实现方式不同,由于 list 容器的元素并不是连续存储的,所以该容器迭代器中,必须包含一个可以指向 list 容器的指针,并且该指针还可以借助重载的 *、++、–、==、!= 等运算符,实现迭代器正确的递增、递减、取值等操作。

template<tyepname T,...>
struct __list_iterator{
    __list_node<T>* node;
    //...
    //重载 == 运算符
    bool operator==(const __list_iterator& x){return node == x.node;}
    //重载 != 运算符
    bool operator!=(const __list_iterator& x){return node != x.node;}
    //重载 * 运算符,返回引用类型
    T* operator *() const {return *(node).myval;}
    //重载前置 ++ 运算符
    __list_iterator<T>& operator ++(){
        node = (*node).next;
        return *this;
    }
    //重载后置 ++ 运算符
    __list_iterator<T>& operator ++(int){
        __list_iterator<T> tmp = *this;
        ++(*this);
        return tmp;
    }
    //重载前置 -- 运算符
    __list_iterator<T>& operator--(){
        node = (*node).prev;
        return *this;
    }
    //重载后置 -- 运算符
    __list_iterator<T> operator--(int){
        __list_iterator<T> tmp = *this;
        --(*this);
        return tmp;
    }
    //...
}

stack

stack没有迭代器,是一种容器适配器,用于在LIFO(后进先出)的操作,其中元素仅从容器的一端插入和提取。

STL总结与常见面试题

stack底层一般用list或deque实现,封闭头部即可,不用vector的原因应该是容量大小有限制,扩容耗时

底层用deque实现

//deque<T> >中间有个空格是为了兼容较老的版本
template <class T, class Sequence = deque<T> >
class stack {
    // 以㆘的 __STL_NULL_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_reference;
protected:
    Sequence c; // 底层容器
public:
    // 以㆘完全利用 Sequence c 的操作,完成 stack 的操作。
    bool empty() const { return c.empty(); }
    size_type size() const { return c.size(); }
    reference top() { return c.back(); }
    const_reference top() const { return c.back(); }
    // deque 是两头可进出,stack 是末端进,末端出(所以后进者先出)。
    void push(const value_type& x) { c.push_back(x); }
    void pop() { 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;
}

底层用list实现

  #include<stack>  
  #include<list>  
  #include<algorithm>  
  #include <iostream>  
  using namespace std;  
    
  int main(){  
      stack<int, list<int>> istack;  
      istack.push(1);  
      istack.push(3);  
      istack.push(5);  
        
      cout << istack.size() << endl; //3  
      cout << istack.top() << endl;//5  
      istack.pop();  
      cout << istack.top() << endl;//3  
      cout << istack.size() << endl;//2  
    
      system("pause");  
      return 0;  
  }  

queue

queue 是一种容器适配器,用于在FIFO(先入先出)的操作,其中元素插入到容器的一端并从另一端提取。

STL总结与常见面试题

队列不提供迭代器,不实现遍历操作。

template <class T, class Sequence = deque<T> >
class queue {
  friend bool operator== __STL_NULL_TMPL_ARGS (const queue& x, const queue& y);
  friend bool operator< __STL_NULL_TMPL_ARGS (const queue& x, const queue& y);
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_reference;
protected:
  Sequence c;
public:
  bool empty() const { return c.empty(); }
  size_type size() const { return c.size(); }
  reference front() { return c.front(); }
  const_reference front() const { return c.front(); }
  reference back() { return c.back(); }
  const_reference back() const { return c.back(); }
  void push(const value_type& x) { c.push_back(x); }
  void pop() { c.pop_front(); }
};

template <class T, class Sequence>
bool operator==(const queue<T, Sequence>& x, const queue<T, Sequence>& y) {
  return x.c == y.c;
}

template <class T, class Sequence>
bool operator<(const queue<T, Sequence>& x, const queue<T, Sequence>& y) {
  return x.c < y.c;
}

priority_queue

优先队列,其底层是用堆来实现的。在优先队列中,队首元素一定是当前队列中优先级最高的那一个。

set

set 是按照特定顺序存储唯一元素的容器。

template<class _Kty,
    class _Pr = less<_Kty>,
    class _Alloc = allocator<_Kty> >
class set
  1. set 的 底层数据结构是 红黑树,一种高效的平衡检索二叉树。
  2. set 容器中 每一个元素就是二叉树的每一个节点,对于set容器的插入删除操作,效率都比较高,原因是因为二叉树的删除插入元素并不需要进行内存拷贝和内存移动,只是改变了指针的指向。
  3. 对 set 进行插入删除操作 都不会引起iterator的失效,因为迭代器相当于一个指针指向每一个二叉树的节点,对set的插入删除并不会改变原有内存中节点的改变, 但是vector的插入删除操作一般会发生内存移动和内存拷贝,所以会发生迭代器的失效。
  4. set容器的检索速度很快,因为采用二分查找的方法 。

multiset

multiset允许元素重复而set不允许

template<class _Kty,
	class _Pr = less<_Kty>,
	class _Alloc = allocator<_Kty> >
class multiset

map

map 是关联容器,按照特定顺序存储由 key value (键值) 和 mapped value (映射值) 组合形成的元素。

由于 RB-tree 是一种平衡二叉搜索树,自动排序的效果很不错,所以标准的STL map 即以 RB-tree 为底层机制。又由于 map 所开放的各种操作接口,RB-tree 也都提供了,所以几乎所有的 map 操作行为,都只是转调 RB-tree 的操作行为。

方法 说明
map 构造函数
begin 返回引用容器中第一个元素的迭代器
key_comp 返回容器用于比较键的比较对象的副本
value_comp 返回可用于比较两个元素的比较对象,以获取第一个元素的键是否在第二个元素之前
find 在容器中搜索具有等于 k的键的元素,如果找到返回一个迭代器,否则返回 map::end
count 在容器中搜索具有等于 k(参数)的键的元素,并返回匹配的数量
lower_bound 返回一个非递减序列 [first, last)中的第一个大于等于值 val的位置的迭代器
upper_bound 返回一个非递减序列 [first, last)中第一个大于 val的位置的迭代器
equal_range 获取相同元素的范围,返回包含容器中所有具有与 k等价的键的元素的范围边界

multimap

multimap 的特性以及用法与 map 完全相同,唯一的差别在于它允许键值重复,因此它的插入操作采用的是底层机制 RB-tree 的 insert_equal() 而非 insert_unique。

unordered_set

unordered_set是基于哈希表,因此要了解unordered_set,就必须了解哈希表的机制。哈希表是根据关键码值而进行直接访问的数据结构,通过相应的哈希函数(也称散列函数)处理关键字得到相应的关键码值,关键码值对应着一个特定位置,用该位置来存取相应的信息,这样就能以较快的速度获取关键字的信息。

template < class Key,  
    class Hash = hash<Key>,  
    class Pred = equal_to<Key>,  
    class Alloc = allocator<Key>  
> class unordered_set;  

4 算法

  1. 简单查找算法,要求输入迭代器(input iterator)
find(beg, end, val); // 返回一个迭代器,指向输入序列中第一个等于 val 的元素,未找到返回 end
find_if(beg, end, unaryPred); // 返回一个迭代器,指向第一个满足 unaryPred 的元素,未找到返回 end
find_if_not(beg, end, unaryPred); // 返回一个迭代器,指向第一个令 unaryPred 为 false 的元素,未找到返回 end
count(beg, end, val); // 返回一个计数器,指出 val 出现了多少次
count_if(beg, end, unaryPred); // 统计有多少个元素满足 unaryPred
all_of(beg, end, unaryPred); // 返回一个 bool 值,判断是否所有元素都满足 unaryPred
any_of(beg, end, unaryPred); // 返回一个 bool 值,判断是否任意(存在)一个元素满足 unaryPred
none_of(beg, end, unaryPred); // 返回一个 bool 值,判断是否所有元素都不满足 unaryPred
  1. 查找重复值的算法,传入向前迭代器(forward iterator)
adjacent_find(beg, end); // 返回指向第一对相邻重复元素的迭代器,无相邻元素则返回 end
adjacent_find(beg, end, binaryPred); // 返回指向第一对相邻重复元素的迭代器,无相邻元素则返回 end
search_n(beg, end, count, val); // 返回一个迭代器,从此位置开始有 count 个相等元素,不存在则返回 end
search_n(beg, end, count, val, binaryPred); // 返回一个迭代器,从此位置开始有 count 个相等元素,不存在则返回 end
  1. 查找子序列算法,除 find_first_of(前两个输入迭代器,后两个前向迭代器) 外,都要求两个前向迭代器
search(beg1, end1, beg2, end2); // 返回第二个输入范围(子序列)在爹一个输入范围中第一次出现的位置,未找到则返回 end1
search(beg1, end1, beg2, end2, binaryPred); // 返回第二个输入范围(子序列)在爹一个输入范围中第一次出现的位置,未找到则返回 end1
find_first_of(beg1, end1, beg2, end2); // 返回一个迭代器,指向第二个输入范围中任意元素在第一个范围中首次出现的位置,未找到则返回end1
find_first_of(beg1, end1, beg2, end2, binaryPred); // 返回一个迭代器,指向第二个输入范围中任意元素在第一个范围中首次出现的位置,未找到则返回end1
find_end(beg1, end1, beg2, end2); // 类似 search,但返回的最后一次出现的位置。如果第二个输入范围为空,或者在第一个输入范围为空,或者在第一个输入范围中未找到它,则返回 end1
find_end(beg1, end1, beg2, end2, binaryPred); // 类似 search,但返回的最后一次出现的位置。如果第二个输入范围为空,或者在第一个输入范围为空,或者在第一个输入范围中未找到它,则返回 end1
  1. 其他只读算法,传入输入迭代器
for_each(beg, end, unaryOp); // 对输入序列中的每个元素应用可调用对象 unaryOp,unaryOp 的返回值被忽略
mismatch(beg1, end1, beg2); // 比较两个序列中的元素。返回一个迭代器的 pair,表示两个序列中第一个不匹配的元素
mismatch(beg1, end1, beg2, binaryPred); // 比较两个序列中的元素。返回一个迭代器的 pair,表示两个序列中第一个不匹配的元素
equal(beg1, end1, beg2); // 比较每个元素,确定两个序列是否相等。
equal(beg1, end1, beg2, binaryPred); // 比较每个元素,确定两个序列是否相等。
  1. 二分搜索算法,传入前向迭代器或随机访问迭代器(random-access iterator),要求序列中的元素已经是有序的
lower_bound(beg, end, val); // 返回一个非递减序列 [beg, end) 中的第一个大于等于值 val 的位置的迭代器,不存在则返回 end
lower_bound(beg, end, val, comp); // 返回一个非递减序列 [beg, end) 中的第一个大于等于值 val 的位置的迭代器,不存在则返回 end
upper_bound(beg, end, val); // 返回一个非递减序列 [beg, end) 中第一个大于 val 的位置的迭代器,不存在则返回 end
upper_bound(beg, end, val, comp); // 返回一个非递减序列 [beg, end) 中第一个大于 val 的位置的迭代器,不存在则返回 end
equal_range(beg, end, val); // 返回一个 pair,其 first 成员是 lower_bound 返回的迭代器,其 second 成员是 upper_bound 返回的迭代器
binary_search(beg, end, val); // 返回一个 bool 值,指出序列中是否包含等于 val 的元素。对于两个值 x 和 y,当 x 不小于 y 且 y 也不小于 x 时,认为它们相等。
  1. 只写不读算法,要求输出迭代器(output iterator)
fill(beg, end, val); // 将 val 赋予每个元素,返回 void
fill_n(beg, cnt, val); // 将 val 赋予 cnt 个元素,返回指向写入到输出序列最有一个元素之后位置的迭代器
genetate(beg, end, Gen); // 每次调用 Gen() 生成不同的值赋予每个序列,返回 void
genetate_n(beg, cnt, Gen); // 每次调用 Gen() 生成不同的值赋予 cnt 个序列,返回指向写入到输出序列最有一个元素之后位置的迭代器

7.使用输入迭代器的写算法,读取一个输入序列,将值写入到一个输出序列(dest)中

copy(beg, end, dest); // 从输入范围将元素拷贝所有元素到 dest 指定定的目的序列
copy_if(beg, end, dest, unaryPred); // 从输入范围将元素拷贝满足 unaryPred 的元素到 dest 指定定的目的序列
copy_n(beg, n, dest); // 从输入范围将元素拷贝前 n 个元素到 dest 指定定的目的序列
move(beg, end, dest); // 对输入序列中的每个元素调用 std::move,将其移动到迭代器 dest 开始始的序列中
transform(beg, end, dest, unaryOp); // 调用给定操作(一元操作),并将结果写到dest中
transform(beg, end, beg2, dest, binaryOp); // 调用给定操作(二元操作),并将结果写到dest中
replace_copy(beg, end, dest, old_val, new_val); // 将每个元素拷贝到 dest,将等于 old_val 的的元素替换为 new_val
replace_copy_if(beg, end, dest, unaryPred, new_val); // 将每个元素拷贝到 dest,将满足 unaryPred 的的元素替换为 new_val
merge(beg1, end1, beg2, end2, dest); // 两个输入序列必须都是有序的,用小于号运算符将合并后的序列写入到 dest 中
merge(beg1, end1, beg2, end2, dest, comp); // 两个输入序列必须都是有序的,使用给定的比较操作(comp)将合并后的序列写入到 dest 中

8.划分算法,要求双向选代器(bidirectional iterator)

is_partitioned(beg, end, unaryPred); // 如果所有满足谓词 unaryPred 的元素都在不满足 unarypred 的元素之前,则返回 true。若序列为空,也返回 true
partition_copy(beg, end, dest1, dest2, unaryPred); // 将满足 unaryPred 的元素拷贝到到 dest1,并将不满足 unaryPred 的元素拷贝到到 dest2。返回一个迭代器 pair,其 first 成员表示拷贝到 dest1 的的元素的末尾,second 表示拷贝到 dest2 的元素的末尾。
partitioned_point(beg, end, unaryPred); // 输入序列必须是已经用 unaryPred 划分过的。返回满足  unaryPred 的范围的尾后迭代器。如果返回的迭代器不是 end,则它指向的元素及其后的元素必须都不满足 unaryPred
stable_partition(beg, end, unaryPred); // 使用 unaryPred 划分输入序列。满足 unaryPred 的元素放置在序列开始,不满足的元素放在序列尾部。返回一个迭代器,指向最后一个满足 unaryPred 的元素之后的位置如果所有元素都不满足 unaryPred,则返回 beg
partition(beg, end, unaryPred); // 使用 unaryPred 划分输入序列。满足 unaryPred 的元素放置在序列开始,不满足的元素放在序列尾部。返回一个迭代器,指向最后一个满足 unaryPred 的元素之后的位置如果所有元素都不满足 unaryPred,则返回 beg
  1. 排序算法,要求随机访问迭代器(random-access iterator)
sort(beg, end); // 排序整个范围
stable_sort(beg, end); // 排序整个范围(稳定排序)
sort(beg, end, comp); // 排序整个范围
stable_sort(beg, end, comp); // 排序整个范围(稳定排序)
is_sorted(beg, end); // 返回一个 bool 值,指出整个输入序列是否有序
is_sorted(beg, end, comp); // 返回一个 bool 值,指出整个输入序列是否有序
is_sorted_until(beg, end); // 在输入序列中査找最长初始有序子序列,并返回子序列的尾后迭代器
is_sorted_until(beg, end, comp); // 在输入序列中査找最长初始有序子序列,并返回子序列的尾后迭代器
partial_sort(beg, mid, end); // 排序 mid-beg 个元素。即,如果 mid-beg 等于 42,则此函数将值最小的 42 个元素有序放在序列前 42 个位置
partial_sort(beg, mid, end, comp); // 排序 mid-beg 个元素。即,如果 mid-beg 等于 42,则此函数将值最小的 42 个元素有序放在序列前 42 个位置
partial_sort_copy(beg, end, destBeg, destEnd); // 排序输入范围中的元素,并将足够多的已排序元素放到 destBeg 和 destEnd 所指示的序列中
partial_sort_copy(beg, end, destBeg, destEnd, comp); // 排序输入范围中的元素,并将足够多的已排序元素放到 destBeg 和 destEnd 所指示的序列中
nth_element(beg, nth, end); // nth 是一个迭代器,指向输入序列中第 n 大的元素。nth 之前的元素都小于等于它,而之后的元素都大于等于它
nth_element(beg, nth, end, comp); // nth 是一个迭代器,指向输入序列中第 n 大的元素。nth 之前的元素都小于等于它,而之后的元素都大于等于它
  1. 使用前向迭代器的重排算法。普通版本在输入序列自身内部重拍元素,_copy 版本完成重拍后写入到指定目的序列中,而不改变输入序列
remove(beg, end, val); // 通过用保留的元素覆盖要删除的元素实现删除 ==val 的元素,返回一个指向最后一个删除元素的尾后位置的迭代器
remove_if(beg, end, unaryPred); // 通过用保留的元素覆盖要删除的元素实现删除满足 unaryPred 的元素,返回一个指向最后一个删除元素的尾后位置的迭代器
remove_copy(beg, end, dest, val); // 通过用保留的元素覆盖要删除的元素实现删除 ==val 的元素,返回一个指向最后一个删除元素的尾后位置的迭代器
remove_copy_if(beg, end, dest, unaryPred); // 通过用保留的元素覆盖要删除的元素实现删除满足 unaryPred 的元素,返回一个指向最后一个删除元素的尾后位置的迭代器
unique(beg, end); // 通过对覆盖相邻的重复元素(用 == 确定是否相同)实现重排序列。返回一个迭代器,指向不重复元素的尾后位置
unique (beg, end, binaryPred); // 通过对覆盖相邻的重复元素(用 binaryPred 确定是否相同)实现重排序列。返回一个迭代器,指向不重复元素的尾后位置
unique_copy(beg, end, dest); // 通过对覆盖相邻的重复元素(用 == 确定是否相同)实现重排序列。返回一个迭代器,指向不重复元素的尾后位置
unique_copy_if(beg, end, dest, binaryPred); // 通过对覆盖相邻的重复元素(用 binaryPred 确定是否相同)实现重排序列。返回一个迭代器,指向不重复元素的尾后位置
rotate(beg, mid, end); // 围绕 mid 指向的元素进行元素转动。元素 mid 成为为首元素,随后是 mid+1 到到 end 之前的元素,再接着是 beg 到 mid 之前的元素。返回一个迭代器,指向原来在 beg 位置的元素
rotate_copy(beg, mid, end, dest); // 围绕 mid 指向的元素进行元素转动。元素 mid 成为为首元素,随后是 mid+1 到到 end 之前的元素,再接着是 beg 到 mid 之前的元素。返回一个迭代器,指向原来在 beg 位置的元素
  1. 使用双向迭代器的重排算法
reverse(beg, end); // 翻转序列中的元素,返回 void
reverse_copy(beg, end, dest);; // 翻转序列中的元素,返回一个迭代器,指向拷贝到目的序列的元素的尾后位置
  1. 使用随机访问迭代器的重排算法
random_shuffle(beg, end); // 混洗输入序列中的元素,返回 void
random_shuffle(beg, end, rand); // 混洗输入序列中的元素,rand 接受一个正整数的随机对象,返回 void
shuffle(beg, end, Uniform_rand); // 混洗输入序列中的元素,Uniform_rand 必须满足均匀分布随机数生成器的要求,返回 void
  1. 最小值和最大值
min(val1, va12); // 返回 val1 和 val2 中的最小值,两个实参的类型必须完全一致。参数和返回类型都是 const的引引用,意味着对象不会被拷贝。下略
min(val1, val2, comp);
min(init_list);
min(init_list, comp);
max(val1, val2);
max(val1, val2, comp);
max(init_list);
max(init_list, comp);
minmax(val1, val2); // 返回一个 pair,其 first 成员为提供的值中的较小者,second 成员为较大者。下略
minmax(vall, val2, comp);
minmax(init_list);
minmax(init_list, comp);
min_element(beg, end); // 返回指向输入序列中最小元素的迭代器
min_element(beg, end, comp); // 返回指向输入序列中最小元素的迭代器
max_element(beg, end); // 返回指向输入序列中最大元素的迭代器
max_element(beg, end, comp); // 返回指向输入序列中最大元素的迭代器
minmax_element(beg, end); // 返回一个 pair,其中 first 成员为最小元素,second 成员为最大元素
minmax_element(beg, end, comp); // 返回一个 pair,其中 first 成员为最小元素,second 成员为最大元素
  1. 字典序比较,根据第一对不相等的元素的相对大小来返回结果。如果第一个序列在字典序中小于第二个序列,则返回 true。否则,返回 fa1se。如果个序列比另一个短,且所有元素都与较长序列的对应元素相等,则较短序列在字典序中更小。如果序列长度相等,且对应元素都相等,则在字典序中任何一个都不大于另外一个。
lexicographical_compare(beg1, end1, beg2, end2);
lexicographical_compare(beg1, end1, beg2, end2, comp);

5 如何选择合适的容器

需要根据容器的特点和使用场景而定,可能满足需求的不止一种容器。

按是否有序关联性分为:

  1. 序列式容器:array、vector、deque、list 和 forward_list;
  2. 关联式容器:map、multimap、set 和 multiset;
  3. 无序关联式容器:unordered_map、unordered_multimap、unordered_set 和 unordered_multiset;
  4. 容器适配器:stack、queue 和 priority_queue。 根据容器底层采用是否是连续的存储空间分为:
  5. 采用连续的存储空间:array、vector、deque;
  6. 采用分散的存储空间:list、forward_list 以及所有的关联式容器和哈希容器。

注意:deque 容器归为使用连续存储空间的这一类,是存在争议的。因为 deque 容器底层采用一段一段的连续空间存储元素,但是各段存储空间之间并不一定是紧挨着的。

STL总结与常见面试题
选择容器流程图(来源于网络)

选择容器的几点建议:

  • 如果只是存储确定或不确定的对象,而不去删除它们,可以选用vector。就是因为vector是数组的替代品,是连续内存的,不适合频繁的删除。
  • 如果在容器的指定位置插入新元素,则只能选择序列式容器,不选择关联式容器和哈希容器。
  • 如果频繁的插入和删除,可以选用list(链表),内存不是连续的,可以方便的插入和删除,但是不支持索引访问。
  • 数据量很大,不在乎他们的排序,要求效率,对容器中各元素的存储位置没有要求,可以考虑使用哈希容器,反之就要避免使用哈希容器。
  • 如果是随机访问迭代器,选择 array、vector、deque。
  • 如果是双向迭代器,考虑 list 序列式容器以及所有的关联式容器。
  • 如果必须是前向迭代器,考虑 forward_list序列式容器以及所有的哈希容器。
  • 如果插入或删除操作时,容器中的其它元素不移动?选择不是array、vector、deque的其它容器。

6 面试中常出现的STL问题

  1. vector的底层原理

vector底层是一个动态数组,包含三个迭代器,start和finish之间是已经被使用的空间范围,end_of_storage是整块连续空间包括备用空间的尾部。

当空间不够装下数据(vec.push_back(val))时,会自动申请另一片更大的空间(1.5倍或者2倍),然后把原来的数据拷贝到新的内存空间,接着释放原来的那片空间【vector内存增长机制】。

当释放或者删除(vec.clear())里面的数据时,其存储空间不释放,仅仅是清空了里面的数据。

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

STL总结与常见面试题
  1. vector中的reserve和resize的区别
  • reserve是直接扩充到已经确定的大小,可以减少多次开辟、释放空间的问题(优化push_back),就可以提高效率,其次还可以减少多次要拷贝数据的问题。reserve只是保证vector中的空间大小(capacity)最少达到参数所指定的大小n。reserve()只有一个参数。
  • resize()可以改变有效空间的大小,也有改变默认值的功能。capacity的大小也会随着改变。resize()可以有多个参数。
  1. vector中的size和capacity的区别
  • size表示当前vector中有多少个元素(finish – start);
  • capacity函数则表示它已经分配的内存中可以容纳多少元素(end_of_storage – start);
  1. vector中erase方法与algorithn中的remove方法区别
  • vector中erase方法真正删除了元素,迭代器不能访问了
  • remove只是简单地将元素移到了容器的最后面,迭代器还是可以访问到。因为algorithm通过迭代器进行操作,不知道容器的内部结构,所以无法进行真正的删除。
  1. vector迭代器失效的情况
  • 当插入一个元素到vector中,由于引起了内存重新分配,所以指向原内存的迭代器全部失效。
  • 当删除容器中一个元素后,该迭代器所指向的元素已经被删除,那么也造成迭代器失效。erase方法会返回下一个有效的迭代器,所以当我们要删除某个元素时,需要it=vec.erase(it);。
  1. 正确释放vector的内存(clear(), swap(), shrink_to_fit())
  • vec.clear():清空内容,但是不释放内存。
  • vector<int>().swap(vec):清空内容,且释放内存,想得到一个全新的vector。
  • vec.shrink_to_fit():请求容器降低其capacity和size匹配。
  • vec.clear();vec.shrink_to_fit();:清空内容,且释放内存。
  1. list的底层原理
STL总结与常见面试题
  • ist的底层是一个双向链表,使用链表存储数据,并不会将它们存储到一整块连续的内存空间中。恰恰相反,各元素占用的存储空间(又称为节点)是独立的、分散的,它们之间的线性关系通过指针来维持,每次插入或删除一个元素,就配置或释放一个元素空间。
  • list不支持随机存取,如果需要大量的插入和删除,而不关心随即存取
  1. 什么情况下用vector,什么情况下用list,什么情况下用deque
  • vector可以随机存储元素(即可以通过公式直接计算出元素地址,而不需要挨个查找),但在非尾部插入删除数据时,效率很低,适合对象简单,对象数量变化不大,随机访问频繁。除非必要,我们尽可能选择使用vector而非deque,因为deque的迭代器比vector迭代器复杂很多。
  • list不支持随机存储,适用于对象大,对象数量变化频繁,插入和删除频繁,比如写多读少的场景。
  • 需要从首尾两端进行插入或删除操作的时候需要选择deque。
  1. priority_queue的底层原理
  • priority_queue:优先队列,其底层是用堆来实现的。在优先队列中,队首元素一定是当前队列中优先级最高的那一个。
  1. map 、set、multiset、multimap的底层原理

map 、set、multiset、multimap的底层实现都是红黑树,epoll模型的底层数据结构也是红黑树,linux系统中CFS进程调度算法,也用到红黑树。

STL总结与常见面试题

红黑树的特性:

  • 每个结点或是红色或是黑色;
  • 根结点是黑色;
  • 每个叶结点是黑的;
  • 如果一个结点是红的,则它的两个儿子均是黑色;
  • 每个结点到其子孙结点的所有路径上包含相同数目的黑色结点。
  1. 为何map和set的插入删除效率比其他序列容器高
  • 因为不需要内存拷贝和内存移动
  1. 为何map和set每次Insert之后,以前保存的iterator不会失效?
  • 因为插入操作只是结点指针换来换去,结点内存没有改变。而iterator就像指向结点的指针,内存没变,指向内存的指针也不会变。
  1. 当数据元素增多时(从10000到20000),map的set的查找速度会怎样变化?
  • RB-TREE用二分查找法,时间复杂度为logn,所以从10000增到20000时,查找次数从log10000=14次到log20000=15次,多了1次而已。
  1. map 、set、multiset、multimap的特点
  • set和multiset会根据特定的排序准则自动将元素排序,set中元素不允许重复,multiset可以重复。
  • map和multimap将key和value组成的pair作为元素,根据key的排序准则自动将元素排序(因为红黑树也是二叉搜索树,所以map默认是按key排序的),map中元素的key不允许重复,multimap可以重复。
  • map和set的增删改查速度为都是logn,是比较高效的。
  1. 为何map和set的插入删除效率比其他序列容器高,而且每次insert之后,以前保存的iterator不会失效?
  • 存储的是结点,不需要内存拷贝和内存移动。
  • 插入操作只是结点指针换来换去,结点内存没有改变。而iterator就像指向结点的指针,内存没变,指向内存的指针也不会变。
  1. 为何map和set不能像vector一样有个reserve函数来预分配数据?
  • 在map和set内部存储的已经不是元素本身了,而是包含元素的结点。也就是说map内部使用的Alloc并不是map<Key, Data, Compare, Alloc>声明的时候从参数中传入的Alloc。
  1. set的底层实现实现为什么不用哈希表而使用红黑树?
  • set中元素是经过排序的,红黑树也是有序的,哈希是无序的
  • 如果只是单纯的查找元素的话,那么肯定要选哈希表了,因为哈希表在的最好查找时间复杂度为O(1),并且如果用到set中那么查找时间复杂度的一直是O(1),因为set中是不允许有元素重复的。而红黑树的查找时间复杂度为O(lgn)
  1. hash_map与map的区别?什么时候用hash_map,什么时候用map? 构造函数:hash_map需要hash function和等于函数,而map需要比较函数(大于或小于)。

存储结构:hash_map以hashtable为底层,而map以RB-TREE为底层。

总的说来,hash_map查找速度比map快,而且查找速度基本和数据量大小无关,属于常数级别。而map的查找速度是logn级别。但不一定常数就比log小,而且hash_map还有hash function耗时。

如果考虑效率,特别当元素达到一定数量级时,用hash_map。

考虑内存,或者元素数量较少时,用map。

  1. 迭代器失效的问题

插入操作:

  • 对于vector和string,如果容器内存被重新分配,iterators,pointers,references失效;如果没有重新分配,那么插入点之前的iterator有效,插入点之后的iterator失效;
  • 对于deque,如果插入点位于除front和back的其它位置,iterators,pointers,references失效;当我们插入元素到front和back时,deque的迭代器失效,但reference和pointers有效;
  • 对于list和forward_list,所有的iterator,pointer和refercnce有效。

删除操作:

  • 对于vector和string,删除点之前的iterators,pointers,references有效;off-the-end迭代器总是失效的;
  • 对于deque,如果删除点位于除front和back的其它位置,iterators,pointers,references失效;当我们插入元素到front和back时,off-the-end失效,其他的iterators,pointers,references有效;
  • 对于list和forward_list,所有的iterator,pointer和refercnce有效。
  • 对于关联容器map来说,如果某一个元素已经被删除,那么其对应的迭代器就失效了,不应该再被使用,否则会导致程序无定义的行为。
  1. 线程不安全的情况
  • 在对同一个容器进行多线程的读写、写操作时;
  • 在每次调用容器的成员函数期间都要锁定该容器;
  • 在每个容器返回的迭代器(例如通过调用begin或end)的生存期之内都要锁定该容器;
  • 在每个在容器上调用的算法执行期间锁定该容器。

参考资料

http://www.cplusplus.com/reference/stl/ https://blog.csdn.net/qq_23350817/article/details/87930715 https://blog.csdn.net/bizhu12/article/details/6769976             http://c.biancheng.net/view/7385.html
https://blog.csdn.net/daaikuaichuan/article/details/80717222

转自:https://mp.weixin.qq.com/s/JWAestcpQTlp2fb1Q45AQg

C++编程新手容易犯的 10 种编程错误

来源:https://blog.csdn.net/chenlycly

 

公司每年都会有一定的人员流动,相应地也会招一些应届生补充进来,指导应届生已经成为老员工的必修课了。平日里会我们会经常帮新人排查代码中的问题,在此过程中发现了 C++ 新手容易犯的一些编程错误,在此简单的总结一下,给新人们提供一个参考。

1、有些关键字在 cpp 文件中多写了

对于 C++ 类,一些关键字只要写在 .h 中就好,cpp 中就不用再加上了,比如 virtual、static 等关键字,如果在 cpp 中多写,编译器会报错。比如如下的虚接口与静态成员变量的定义,只要在头文件中声明就可以了。

class shape
{
    virtual Draw();
    //...
    static int nLevel;
}

2、函数参数的默认值写到函数实现中了

带有参数默认值的函数,默认值是加在函数声明处的,函数实现处的参数是不需要带上的。为了方便查看代码,在函数实现处的参数中,将默认值注释起来。正确的做法是,头文件中有默认值:

BOOL CreateConf( const CString& strConfName, const BOOL bAudio = FALSE );
在函数实现处的参数中不用添加默认值:
BOOL CreateConf( const CString& strConfName, const BOOL bAudio/* = FALSE*/ );
{
    // ......
}

3、在编写类的时候,在类的结尾处忘记添加 “;” 分号了

在类的结尾处忘记添加分号,编译会报错,新人们有可能找了半天也没找出引起编译错误的原因。其实很简单,在类的结尾处忘记添加分号了。

class Shape
{
    // ...
};

4、只添加了函数声明,没有函数实现

在添加类的函数时,只在类的头文件中添加了函数声明,但在 cpp 中却没有添加函数的实现。如果其他地方调用到该函数,在编译链接的时候会报unresolved external symbol错误。因为没有实现,所有没有供链接使用的 obj 文件。

5、cpp 文件忘记添加到工程中,导致没有生成供链接使用的 obj 文件

在添加 C++ 类时,我们一般会添加 .h 头文件和一个 .cpp 源文件。结果忘记把 .cpp 文件添加到工程中了,即没有参与编译,没有生成供链接使用的 obj 文件。如果有代码调用到该 C++ 类的接口,则在编译链接的时候会报unresolved external symbol错误,即链接不到该 C++ 类对应的接口。

6、函数中返回了一个局部变量的地址或者引用

在函数中返回了一个局部变量的地址或者引用,而这个局部变量在函数结束时其生命周期就结束了,内存就被释放了。当外部访问到该变量的内存,会触发内存访问违例的异常,因为该变量的内存已经释放了。比如如下的错误代码:

char* GetResult()
{
    char chResult[100] = { 0 };

    // ......

    return chResult;
}

7、忘记将父类中的接口声明 virtual 函数,导致多态没有生效

代码中本来要借助于 C++ 多态的虚函数调用,调用子类实现的接口,结果忘记在父类中将对应的接口声明为 virtual,导致没有调用到子类实现的函数。一定要记住,要实现多态下的函数调用,父类的相关接口必须声明为 virtual。

class Shape()
{
    // ...

    virtual void Draw();

    // ...
}

8、该使用双指针的地方,却使用了单指针

有时我们需要调用一个接口去获取某些数据,接口中将数据拷贝到传入的参数对应的内存中,此时设计参数时会传入指针或引用。我们在调用GetData 之前定义了结构体指针p,并 new 出了对应的结构体对象内存,应该在定义 GetData 接口时应该使用双指针(指针的指针)的,结果错写成了单指针。

有问题的代码如下:

struct CodecInfo     // 编码信息
{
    int nFrameRate;

    // ...
}


CodecInfo* pInfo = new CodecInfo;

GetAudioCodecPtr()->GetCodecInfo(pInfo);   // 调用AudioCodec::GetCodecInfo获取编码信息


AudioCodec::GetCodecInfo( CodecInfo* pInfo)  // 此处的参数不应该使用单指针
{
    memcpy(pInfo, m_codecInfo, sizeof(CodecInfo));
}

上面中的AudioCodec::GetCodecInfo接口的参数不应该为单指针,应该用双指针,修改后的代码应该如下:

AudioCodec::GetCodecInfo( CodecInfo** pInfo)  // 此处的参数类型使用双指针
{
    memcpy(*pInfo, m_codecInfo, sizeof(CodecInfo));
}

9、发布 exe 程序时,忘记将 exe 依赖的 C 运行时库和 MFC 库带上

比如新人用 VS-MFC 库编写一个测试用的工具软件,结果在发布 release 版本程序时,没有将程序依赖的 C 运行时库带上,导致该工具软件在某些电脑中启动报错,提示找不到 C 运行时库:

因为程序中依赖了动态版本的运行时库和 MFC 库,在发布程序时要将这些库带上。有些系统中没有这些库,程序启动时就会报找不到库,就会启动失败。

10、应该使用深拷贝,却使用了浅拷贝

本来应该要进行深拷贝的,却使用了浅拷贝(直接赋值),导致另个不同生命周期的 C++ 对象指向了同一块内存,一个对象将内存释放后,另一个对象再用到这块内存,就造成了内存访问违例,产生异常。

有个经典的 C++ 笔试题,让我们实现 String 类的相关函数,其主要目的就是用来考察对深拷贝与浅拷贝的理解的。题目中给出 String类的声明:

class String{
public:
    String();
    String(const String & str);
    String(const char* str);
    String& operator=(String str);
    char* c_str() const;
    ~String();
    int size() const;
private:
    char* data;
};

让写出上述几个函数的内部实现。这些函数的实现代码如下:

//普通构造函数  
String::String(const char *str)
{
  if (str == NULL)
  {
    m_data = new char[1];// 得分点:对空字符串自动申请存放结束标志''的,加分点:对m_data加NULL判断  
    *m_data = '';
  }
  else
  {
    int length = strlen(str);
    m_data = new char[length + 1];// 若能加 NULL 判断则更好
    strcpy(m_data, str);
  }
}
 
 
// String的析构函数  
String::~String(void)
{
  delete[] m_data; // 或delete m_data;  
}
 
 
//拷贝构造函数  
String::String(const String &other)// 得分点:输入参数为const型  
{     
  int length = strlen(other.m_data);
  m_data = new char[length + 1];// 若能加 NULL 判断则更好  
  strcpy(m_data, other.m_data);
}
 
 
//赋值函数  
String & String::operator = (const String &other) // 得分点:输入参数为const型  
{
  if (this == &other)//得分点:检查自赋值  
    return *this; 
  if (m_data)
      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;//得分点:返回本对象的引用    
}

简单分享快乐学习,如有错误请多包涵!

PS:欢迎分享,点赞,在看。

 

转自:https://mp.weixin.qq.com/s/tUT9sDR-d1aTzWof3TCjNg