Background
在秋招之前希望能够达到写出业务c++水平的代码。
Content
1. 基本认知
1. 基本常识
1 为什么感觉c++学了很久没有用
- *C/C++ 整套的语法不具备“功能完备性”,单纯地使用这门语言本身提供的功能无法创建任何有意义的程序,必须借助操作系统的 API 接口函数来达到相应的功能。 **当然,随着 C++ 语言标准和版本的不断更新升级,这种现状正在改变;而像 Java、Python 这类语言,其自带的 SDK 提供了各种操作系统的功能。举个例子,C/C++ 语言本身不具备网络通信功能,必须使用操作系统提供的网络通信函数(#include<system/socket>如 Socket 系列函数);而对于 Java 来说,其 JDK 自带的 java.net 和 java.io 等包则提供了完整的网络通信功能。我在读书的时候常常听人说,QQ、360 安全卫士这类软件是用 C/C++ 开发的,但是当我学完整本 C/C++ 教材以后,仍然写不出来一个像样的窗口程序。许多过来人应该都有类似的困惑吧?其原因是一般 C/C++ 的教材不会教你如何使用操作系统 API 函数的内容。**C/C++ 语言需要直接使用操作系统的接口功能,这就造成了 C/C++ 语言繁、难的地方。**如操作内存不当容易引起程序宕机,不同操作系统的 API 接口使用习惯和风格也不一样。接口函数种类繁多,开发者如果想开发跨平台的程序,必须要学习多个平台的接口函数和对应的系统原理。Java 这类语言,很多功能即使操作系统提供了,如果 Java 虚拟机不提供,开发人员也无法使用。
2 不要写精通c++,语言精通也没啥用,这个语言也不是为了让你精通的。
2. 程序编译流程
basic points
关于程序编译这方面的知识是程序员的基本修养, 虽然不了解对写代码没什么大的影响,但是对于一些高薪的工作岗位必然会对这方面是不低的要求, 因为一些大型的项目是需要结合这些编译原理去优化代码和调试代码。 如果不知道程序背后的工作, 很难解决非业务级别的问题。这我推荐一些比较好的参考资料: here 这里面的最后一栏详细介绍了编译原理的过程。此外有本腾讯的后台核心开发里面也涉及到了这方面的知识。具体细节就不在此展开。
面试题
- 系统大小端是什么?如何检测
A: 大端是低地址存高位, 小段是低地址存放低位。网络传输都是采用大字节。
//大端是从大到小。 big2end .
//方法一:联合体union判断第一位
int CheckSystem()
{
union Check
{
//定义两个变量(i和ch),这两个变量共用同一块地址空间,并且两个变量的首地址是一样的
int i;
char ch;
}c;
// 将1放到i的低位去
c.i = 1;
//倘若ch的最低位是1,则证明机器为小端存储模式;否则反之
return (c.ch == 1);
}
- 程序编译链接的过程?
- A: 预处理(预处理如 #include、#define 等预编译指令,生成 .i 或 .ii 文件)
编译(编译器进行词法分析、语法分析、语义分析、中间代码生成、目标代码生成、优化,生成 .s 文件)
汇编(汇编器把汇编码翻译成机器码,生成 .o 文件)
链接(链接器对未分配的变量分配绝对地址, 这个过程主要是把各个模块之间相互引用部分处理好,地址对应上。 最后生成 .out 文件)
- 链接的过程是怎样的?可执行文件定义elf文件格式标准是怎么分配?(不怎么会考到)
- 对于独立编译的可重定位目标文件,其ELF文件格式包括ELF头(指定文件大小及字节序)、.text(代码段)、.rodata(只读数据区)、.data(已初始化数据区)、.bss(未初始化全局变量)、.symtab(符号表)等,其中链接时最需要关注的就是符号表。每个可重定位目标文件都有一张符号表,它包含该模块定义和引用的符号的信息,简而言之就是我们在每个模块中定义和引用的全局变量(包括定义在本模块的全局变量、静态全局变量和引用自定义在其他模块的全局变量)需要通过一张表来记录,在链接时通过查表将各个独立的目标文件合并成一个完整的可执行文件。
- 可执行文件定义elf文件格式标准是怎么分配?
- ELF头部: 描述文件总体格式,并且包括程序的入口点(entry point),也就是程序运行时执行的第一条指令地址。
- 段头部表: 描述了可执行文件数据段、代码段等各段的大小、虚拟地址、段对齐、执行权限等。实际上通过段头部表描绘了虚拟存储器运行时存储映像,比如每个UNIX程序的代码段总是从虚拟地址Ox0804800开始的。
- 其他段: 和可重定位目标文件各段基本相同,但完成了多个节的合并和重定位工作。
- 动态库和静态库的概念与应用场景?
库就是许多常用代码生成的目标文件打包,使用ar创建。.a 文件是静态库, 在编译的时候将内容已经复制到目标文件中了, 静态库封装的函数在包含了静态库头文件之后(做函数声明)就可以直接使用了。动态库是在程序运行的时候才进行链接。
静态库执行速度快但是文件大。 动态库适合接口维护并且多进程调用时候能做到一次调用多程序使用。
-
makefile的作用?
makefile 可以决定程序的编译流程, 通过脚本极大简化的编译的复杂程度。此外单纯的手写makefile还是比较难,因此很多人通过cmake去生成makefile。 但是一些大型开源项目如果有问题一定是要通过makefile去分析问题的。 -
什么事目标文件?
- . 目标文件包含了三种类型。分别是:
库(lib ,.a ,.so ,dll): 能够提供常用服务接口的包。
可执行文件(exe, .out) : 很多了。
可重新定位的目标文件.o(windows .obj), 它既可以被拿来链接成可执行文件,也可以生成库。
这三个都遵循elf格式,通过一些工具和先验知识可以针对这些目标文件进行特殊处理,例如压缩。
- 堆和栈的区别?
-
申请方式不同。栈由系统自动分配。堆由程序员手动分配。
-
申请大小限制不同。栈顶和栈底是之前预设好的,大小固定,可以通过ulimit -a查看,由ulimit -s修改。堆向高地址扩展,是不连续的内存区域,大小可以灵活调整。
-
申请效率不同。栈由系统分配,速度快,不会有碎片。堆由程序员分配,速度慢,且会有碎片。
- 引用的头文件和库的区别
- 引用的头文件会去库里面找相关的函数定义,如果找不到的话会在本项目中找,如果没有的话就报错。 注意cpp头文件的引用和python的import作用是一样的
- 程序运行的原理与分区
- 每个程序会在内存中加载为进程, 进程的虚拟地址空间是由大量的准确定义的区构成,linux从低地址到高地址依次为:程序代码和数据;堆;共享库;栈;内核虚拟存储器。不同的程序代码结构相同,只不过这几个地方的内容不一样。
栈:存放函数的局部变量、函数参数、返回地址等,由编译器自动分配和释放。
堆:动态申请的内存空间,就是由 malloc 分配的内存块,由程序员控制它的分配和释放,如果程序执行结束还没有释放,操作系统会自动回收。
全局区/静态存储区(.bss 段和 .data 段):存放全局变量和静态变量,程序运行结束操作系统自动释放,在 C 语言中,未初始化的放在 .bss 段中,初始化的放在 .data 段中,C++ 中不再区分了。
常量存储区(.data 段):存放的是常量,不允许修改,程序运行结束自动释放。
代码区(.text 段):存放代码,不允许修改,但可以执行。编译后的二进制文件存放在这里。
- 内存泄漏的定义?如何防止
并非指内存从物理上消失,而是指程序在运行过程中,由于疏忽或错误而失去了对该内存的控制,从而造成了内存的浪费。常指堆内存泄漏,因为堆是动态分配的,而且是用户来控制的,如果使用不当,会产生内存泄漏。防止方法:内部封装:将内存的分配和释放封装到类中,在构造的时候申请内存,析构的时候释放内存。
3. 预处理与头文件
basic points
- 注释分为 // 和 /* */ 一般在每个文件中添加自己的个人信息显得专业一些,函数和statement注释的方式不一样要注意了。
/**************************************************************************
Copyright: SenseTime
Author: Peng Liu
Date:2021-01-09
Description:Provide class details about c++ grammar.
**************************************************************************/
-
常见的错误分为 syntax error, type error,declaration error。
-
头文件是用来声明函数的, 一些函数需要被其他cpp调用,不能一直都直接在其他cpp里面写吧,所以打包到放到一起,方便调用,也方便让人知道函数是在哪里的。 此外头文件的#pragma one 是防止多次调用的。 或者用ifndef endif 去防止多次调用。 <> 是在标准头文件目录中搜索, " " is search in 自定义的。 cpp 标准库不用加.h, c标准库要加.h 。 注意 < iostream > 和 “iostream.h” 其实是不同的头文件,一个是c++,另外一个是c的。
-
#ifdef _cplusplus是预定义的宏,可以解决c语言不支持重载问题。
-
using namespace 可以防止变量重复。想要直接使用标准库的东西必须启动stl空间。 c语言中的iostream.h 直接是全局的。 cin ,cout 是已经实例化的对象。
设计技巧
-
最好不要在头文件里面加那么using namespace, 容易冲突。
-
宏定义如果是表达式要加(), 不然可能会出现歧义错误。如果是带参数的宏定义要在每个参数加上(),不然传入参数如果是表达式会得到不一样的答案。
-
一般解决bug时候先从最前面,因为bug有传递效应,解决一个就编译一下。
-
#ifdef _DEBUG_可以用来定义调试和发行版本, 在发行版本的时候把def __DEBUG__去掉, 不打印debug信息。
无
面试题
Q1:头文件设计有什么技巧嘛?
A: 一般头文件是放函数声明和类定义的。通过 ifdef , #pramra one 去避免重复编译, 命名空间不要放在这里面, 避免重复。
Q : 宏定义和函数有何区别?
-
宏在编译时完成替换,之后被替换的文本参与编译,相当于直接插入了代码,运行时不存在函数调用,执行起来更快;函数调用在运行时需要跳转到具体调用函数。
-
宏函数属于在结构中插入代码,没有返回值;函数调用具有返回值。
-
宏函数参数没有类型,不进行类型检查;函数参数具有类型,需要检查类型。
Q :宏定义和const区别?
-
宏替换发生在编译阶段之前,属于文本插入替换;const作用发生于编译过程中。
-
宏不检查类型;const会检查数据类型。
-
宏定义的数据没有分配内存空间,只是插入替换掉;const定义的变量只是值不能改变,但要分配内存空间。
Q: 头文件和源文件存放标准?
其实代码完全可以写到一个文件中(cpp, .h),用cpp是为了接口分离。
include<>的从系统库加载
include""从当前工作路径加载(失败了再转到系统路径)
Q : 条件编译的作用
- 发布和调试版本控制还有就是防止重定义
2. Basic knowledge
主要看英文的c++ primer。这里有一个练习仓库,如果看不下去的话看这个笔记也是非常快速的。 此外here也有个中文笔记也不错。 最后我们还可以借鉴:整体面试手册
1. 变量
basic points
这章总结了基础类型和复合类型如何结合关键字去定义。 并且介绍了c++11增加了几个额外的功能。
-
c++基本类型分为字符型char、宽字符型string、整型int、浮点型 float、双浮点型double (8 weight)、布尔型bool(1 bit)。一字节为 8 位。long int 8 个字节,int 是 4 个字节,不同系统会有所差异。 高级类型:枚举、指针、数组、引用、数据结构、类。
-
在 C++ 中,有两种简单的定义常量的方式:#define和 const。
-
string本质是一个类, 但是它基本可以看成基本数据类型。因为很多基本类型的操作她都实现了。 对于这个类还有一个cctype处理string对象中的字符。
-
C++ 提供了以下两种类型的字符串:C 风格字符串(字符串数组, 以\0结尾), 和C++ 引入的 string 类来操作。二者都可以,但是第二个更为方便,第一个要手动或者调用stl实现字符串拼接等功能。字符统计中,strlen不统计\0, 但是sizeof统计。
-
需要注意有些没有图符的常量,例如转义字符和控制字符。
-
由于语法的混乱,很多人分不清初始化和赋值,二者其实是不同的,初始化= 创建变量+ 赋值。
-
大型项目中,变量可以在多个文件extern声明,但是定义只有一次, 这里切记不要重复定义。
-
一个变量只有几种可能的值,可以定义为枚举(enumeration)类型。
enum color { red, green, blue } c;
c = blue;
-
进制规则: 整数常量可以是十进制、八进制或十六进制的常量。前缀指定基数:0x 或 0X 表示十六进制,0 表示八进制,不带前缀则默认表示十进制。
-
左值和右值定义: 左值(l-value)可以出现在赋值语句的左边或者右边,比如变量;右值(r-value)只能出现在赋值语句的右边,比如常量。
-
引用和指针是基于其他基本类型的复合类型。程序就是运行在内存中的, 指针是一个存放内存地址的整数变量(也有地址,但是地址内存放的是指向的地址值),其值为另一个变量的地址,即内存位置的直接地址。就像其他变量或常量一样,必须在使用指针存储其他变量地址之前,对其进行声明。
-
**指针就是创建一个指向指针变量的变量。void*指针是不能做具体工作的花瓶,他能接收所有类型的变量地址,但是实际用的话不行,因为不知道具体类型。如果要赋给其他类型指针,则需要强制类型转换。
-
c++类型限定符 :const const 类型的对象在程序执行期间不能被修改,生存周期只在本文件内,非要访问,必须在指定const前加extern。
-
C++11新标准规定,允许将变量声明为constexpr类型以便由编译器来验证变量的值是否是一个常量的表达式。
-
传统别名:使用typedef来定义类型的同义词。 typedef double wages; 用来解决变量命名过于复杂,含义不清楚情况下。现在使用: using SI = Sales_item;(C++11)
-
存储类定义 C++ 程序中变量/函数的范围(可见性)和生命周期, 它包含
auto(c++11) :声明变量时根据初始化表达式自动推断该变量的类型、声明函数时函数返回值的占位符。 -
volatile:修饰符 volatile 告诉编译器不需要优化volatile声明的变量,让程序可以直接从内存中读取变量。对于一般的变量编译器会对变量进行优化,将内存中的变量值放在寄存器中以加快读写效率。用于经常调用的变量如计数器。
-
register :存储类用于定义存储在寄存器中而不是 RAM 中的局部变量。这意味着变量的最大尺寸等于寄存器的大小(通常是一个词),且不能对它应用一元的 ‘&’ 运算符(因为它没有内存位置)。
-
static: 不需要在每次它进入和离开作用域时进行创建和销毁。因此,使用 static 修饰局部变量可以在函数调用之间保持局部变量的值。 如果只在本文件使用的全部变量要声明成static, 这样可以防止报错和消耗, 全局变量是很危险和难以阅读的一个变量。 static声明的函数内变量虽然具有全局周期,但是活动范围只能是本文件内部。
-
extern : extern 修饰符通常用于当有两个或多个文件共享相同的全局变量或函数的时候
mutable -
thread_local (C++11) :说明符声明的变量仅可在它在其上创建的线程上访问。 变量在创建线程时创建,并在销毁线程时销毁。每个线程都有其自己的变量副本。
-
高级数据结构: 结构体和类是我们常用的。
设计技巧
- 请注意,把常量定义为大写字母形式。
- 一般整形计算用int , 浮点运算用float。
- 建议:初始化所有指针。
- 多个函数或者类共同计数的变量可以用static, 避免全局变量太多,影响可读性。静态数据成员就是为了让同类的对象通信的。
- 当输入变量是同一类型但是不知道具体数量时候可以用while(cin>>val){}.
- malloc() 函数在 C 语言中就出现了,在 C++ 中仍然存在,但建议尽量不要使用 malloc() 函数。new 与 malloc() 函数相比,其主要的优点是,new 不只是分配了内存,它还创建了对象。
- 中文的编码要用wchar* 而不是char* 去编码。
面试题
Q : 变量声明和定义区别?
- 声明仅仅是把变量的声明的位置及类型提供给编译器,并不分配内存空间;定义要在定义的地方为其分配存储空间。
- 相同变量可以再多处声明(外部变量extern),但只能在一处定义。
Q: strlen和sizeof区别?
-
sizeof是运算符,并不是函数,结果在编译时得到而非运行中获得;strlen是字符处理的库函数。
-
sizeof参数可以是任何数据的类型或者数据(sizeof参数不退化);strlen的参数只能是字符指针且结尾是’\0’的字符串。
-
因为sizeof值在编译时确定,所以不能用来得到动态分配(运行时分配)存储空间的大小。
Q:指针和引用的应用场景?
- 引用是变量的别名。 使用时候必须初始化,不能修改。 用于函数传参时候为了避免数据的拷贝。 如果是函数内部不改变数据的话,一定要声明成const 。 指针是存储地址的变量, 通过* 符号去解析存放的地址里面写入的值。
Q:区别以下几种变量?
const int a;
int const a;
const int *a;
int *const a;
-
int const a和const int a均表示定义常量类型a。
-
const int a,其中a为指向int型变量的指针,const在 左侧,表示a指向不可变常量。(看成const (*a),对引用加const)
-
int *const a,依旧是指针类型,表示a为指向整型数据的常量指针。(看成const(a),对指针const)
Q: 数组和指针的区别?
-
数组就是指针, 数组在传参的时候会退化成指针,无法直到其尺寸。 因此我们还会传另外一个size 到函数中。
-
数组名不是真正意义上的指针,可以理解为常指针,所以数组名没有自增、自减等操作。
-
当数组名当做形参传递给调用函数后,就失去了原有特性,退化成一般指针,多了自增、自减操作,但sizeof运算符不能再得到原数组的大小了。
Q: 区别以下指针类型?
int *p[10]
int (*p)[10]
int *p(int)
int (*p)(int)
-
int *p[10]表示指针数组,强调数组概念,是一个数组变量,数组大小为10,数组内每个元素都是指向int类型的指针变量。
-
int (*p)[10]表示数组指针,强调是指针,只有一个变量,是指针类型,不过指向的是一个int类型的数组,这个数组大小是10。
-
int p(int)是函数声明,函数名是p,参数是int类型的,返回值是int 类型的。
-
int (*p)()是函数指针,强调是指针,该指针指向的函数具有int类型参数,并且返回值是int类型的。
Q : 请你说一下static , voliate , inline , const ,register 的作用?
- static : C++的static可以修饰类成员(静态成员变量和静态成员函数),静态成员变量和静态成员函数不属于任何一个对象,是所有类实例所共有。还可以延长变量的声明周期到程序结束,还有对于静态全局变量,相对于全局变量其可见范围被缩小,只能在本文件中可见;修饰函数时作用和修饰全局变量相同,都是为了限定访问域。
- static的数据记忆性可以满足函数在不同调用期的通信,也可以满足同一个类的多个实例间的通信。
- 未初始化时,static变量默认值为0。
- voliate是不要内存优化, volatile定义变量的值是易变的,每次用到这个变量的值的时候都要去重新读取这个变量的值,而不是读寄存器内的备份。多线程中被几个任务共享的变量需要定义为volatile类型。
- register是将变量声明成寄存器,加速。
- inline是展开函数成语句。
Q : malloc和new的区别?
-
malloc和free是标准库函数,支持覆盖;new和delete是运算符,并且支持重载。
-
malloc仅仅分配内存空间,free仅仅回收空间,不具备调用构造函数和析构函数功能,用malloc分配空间存储类的对象存在风险;new和delete除了分配回收功能外,还会调用构造函数和析构函数。
-
malloc和free返回的是void类型指针(必须进行类型转换),new和delete返回的是具体类型指针。
Q :delete和delete[]区别?
-
delete只会调用一次析构函数。
-
delete[]会调用数组中每个元素的析构函数。
Q : 针对变量一部分的话 ,c++11 加了哪些东西?
智能变量auto多用来迭代器的前缀自动分析 , 线程变量等。 统一的列表初始化方式 int a= 1 可以用int a{1}
Q : 常量指针和指针常量区别?
-
常量指针是一个指针,读成常量的指针,指向一个只读变量。如int const * p或const int p。
-
指针常量是一个不能给改变指向的指针。如int *const p。
Q : a和&a有什么区别?
假设数组int a[10];
int (*p)[10] = &a;
-
a是数组名,是数组首元素地址,+1表示地址值加上一个int类型的大小,如果a的值是0x00000001,加1操作后变为0x00000005。*(a + 1) = a[1]。
-
&a是数组的指针,其类型为int (*)[10](就是前面提到的数组指针),其加1时,系统会认为是数组首地址加上整个数组的偏移(10个int型变量),值为数组a尾元素后一个元素的地址。
Q :结构体内存对齐问题
- 结构体变量的首地址能够被其最宽基本类型成员大小与对齐基数中的较小者所整除; 某些硬件设备只能存取对齐数据,存取非对齐的数据可能会引发异常;便于在不同的平台之间进行移植,因为有些硬件平台不能够支持任意地址的数据访问,只能在某些地址处取某些特定的数据,否则会抛出异常;
Q: struct 和 union 的区别
联合体和结构体都是由若干个数据类型不同的数据成员组成。使用时,联合体只有一个有效的成员;而结构体所有的成员都有效。
2. 表达式和语句
本小结包含运算符的使用, 除了一些基本的运算符, 对于一些非内置数据类型还有一些标准库实现的运算符。
basic points
-
C++的表达式分为右值(rvalue)和左值(lvalue)。当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值时,用的是对象的地址。需要右值的地方可以用左值代替,反之则不行。
-
遍历字符串:使用范围for(range for)语句: for (auto c: str),或者 for (auto &c: str)使用引用直接改变字符串中的字符。(c++11)
-
条件运算符 ? : 可以用来替代 if…else 语句。
-
逗号运算符,含有两个运算对象,按照从左向右的顺序依次求值,最后返回右侧表达式的值。逗号运算符经常用在for循环中。
-
**类型转换中隐形是自动的,显性是程序中自己定义的。
-
Lambda 表达式:本质上与函数声明非常类似。为什么要用它呢?
a. 有些时候函数无法使用的地方,lambda表达式依然可以使用,而且更方便简洁
b. 函数要起名,但这是一个很困难的事情,而且容易重名,但是lambda表达式就不太是一个问题。
c. lambda表达式只需要一行,增强了代码的可读性。lambda表达式表示一个可调用的代码单元,可以理解成是一个未命名的内联函数。最简单的 Lambda后面不加 () 的 Lambda 表达式相当于函数指针,加上 () 就相当于调用这个 Lambda 函数。
int main(void) {
// Function point
[](){ cout << "Hello World!" << endl; };
// Call function
[](){ cout << "Hello World!" << endl; }();
return 0;
}
Lambda 可以赋值可以将 Lambda 表达式赋值给一个变量,之后这个变量可以当作函数指针来调用,需要加上 ()。
int main(void) {
auto fun = [](){ cout << "Hello World!" << endl; };
fun();
return 0;
}
Lambda 是 const 函数等等,lambda
- 语句有很多, 包括异常处理语句, 循环语句等。 其中for (declaration : expression)
statement 是和for(i=0;i<k;i++)的升级版本。
设计技巧
- 循环时候不为true可以是任意非0的值,而且可以省略不写。 if(a){}
- 显性转换是非常不推荐的,和goto一个性质, 破坏了程序的可读性。**
- 异常可以避免程序的崩溃, 在一些关键的地方可以加入异常处理。
面试题
Q2 : for(const auto &iter : m) 相比较for(iter =m.begin(); iter!=m.end();i++ )的区别?
A: 第一个更为简便一些,auto和foreach都是cpp11的语法。 引用也更为降低空间消耗。 开发中第一个是经常使用的。
3. 函数
basic points
- 函数的实参和形参区别是,形参只有在调用时候才分配内存, 结束后立即释放。
- 当形参是引用类型时,它对应的实参被引用传递(passed by reference),函数被传引用调用(called by reference)。引用形参是它对应实参的别名。当形参不是引用类型时,形参和实参是两个相互独立的对象,实参的值会被拷贝给形参(值传递,passed by value),函数被传值调用(called by value)。
- main函数中第二个形参argv是一个数组,数组元素是指向C风格字符串的指针;第一个形参argc表示数组中字符串的数量。
int main(int argc, char argv[]) { /…/ }
int main(int argc, char**argv) { /…*/ }
- 函数内,局部变量的值会覆盖全局变量的值。当局部变量被定义时,系统不会对其初始化,您必须自行对其初始化。定义全局变量时,系统会自动初始化为0。
- C++ 标准库提供了大量的程序可以调用的内置函数。函数参数中如果函数要使用参数,有三种传递方式: 传值, 指针, 引用。
- .如果我们不知道自己传参的具体数量, c++11中提供了initializer_list 去代指后续一系列相同type的数据类型。使用方法如下:
void err_msg(ErrCode e, initializer_list<string> il){
cout << e.msg << endl;
for (auto bed = il.begin(); beg != il.end(); ++ beg)
cout << *beg << " ";
cout << endl;
}
err_msg(ErrCode(0), {"functionX", "okay});
-
有返回值函数: return语句的返回值的类型必须和函数的返回类型相同,或者能够隐式地转换成函数的返回类型。 不要返回局部对象的引用或指针,函数调用结束之后会被销毁。
-
分离式编译(Separate Compilation)分离式编译允许我们把程序按照逻辑关系分割到几个文件中去,每个文件独立编译。这一过程通常会产生后缀名是*.obj或.o*的文件,该文件包含对象代码(object code)。之后编译器把对象文件链接(link)在一起形成可执行文件。
-
如果一个函数调用了它自身,不管这种调用是直接的还是间接的,都称该函数为递归函数(recursive function)。在递归函数中,一定有某条路径是不包含递归调用的,否则函数会一直递归下去,直到程序栈空间耗尽为止。相对于循环迭代,递归的效率较低。但在某些情况下使用递归可以增加代码的可读性。循环迭代适合处理线性问题(如链表,每个节点有唯一前驱、唯一后继),而递归适合处理非线性问题(如树,每个节点的前驱、后继不唯一)。
-
因为数组不能被拷贝,所以函数不能返回数组,但可以返回数组的指针或引用。
-
函数指针: 要想声明一个可以指向某种函数的指针,只需要用指针替换函数名称即可。
设计技巧
- 由于数组没有办法拷贝, 传输数组的时候实际上在传输指针, 这样就会导致数组的大小没有办法传递过去, 常用的一个方法是添加一个形参指定长度。
- 很多时候我们希望函数能够改变传入的参数值,这时候用引用会比指针操作更让人理解和阅读。使用引用的时候,函数不会创建形参, 一般如果传输数据量较大的时候使用引用效率会更高。如果既要提高效率, 又不希望传递的数据在函数中不被改变就使用const引用(能定义尽可能定义成const)。把函数不会改变的形参定义成普通引用会极大地限制函数所能接受的实参类型,同时也会给别人一种误导,即函数可以修改实参的值。
- 在参数固定但是类型不同的时候使用函数模板更方便。而不是函数重载。
- 由于函数调用时候是需要跳转的,这样有时候也很费时间,因此函数要是那些重复调用的功能。 此外如果经常调用,要用inline去优化展开。 避免来回跳转。inline函数应该在头文件中定义。
面试题
Q1 : 函数重载和函数模板的区别?
- 函数模板适用于不同类型,数量相同的函数, 函数重载适用于不同类型的,数量不相同的情景。
Q2 : 形参和实参的区别?
- 形参是函数调用时候才启动的, 实参是传入给形参的。 如果形参是引用,那么就不用拷贝了。
Q3 . 函数传参的时候要注意什么?
- 需要注意如果是数组就得传入一下大小, 如果数据量大考虑引用传入, 如果函数不改变其内容一定要使用const传入。
Q 函数调用的过程
- 比较复杂,here
Q 重载、重写、隐藏的区别
- 重写就是多态重写, 隐藏就是继承覆盖
Q : 参数传递时,值传递、引用传递、指针传递的区别?
- 值传递:形参是实参的拷贝,函数对形参的所有操作不会影响实参。
指针传递:本质上是值传递,只不过拷贝的是指针的值,拷贝之后,实参和形参是不同的指针,通过指针可以间接的访问指针所指向的对象,从而可以修改它所指对象的值。
引用传递:当形参是引用类型时,我们说它对应的实参被引用传递。
Q : callback 定义?
- 一般函数:function a(int a, String b):接收的参数是一般类型.特殊函数:function b(function c):接收的参数是一个函数,c这个函数就叫回调函数.你也可以这么理解:本质区别是,一般一个函数调用另一个函数,被调用的函数是出现在方法体当中,而回调函数比较特殊,它是出现在参数列表当中.也就是说,当调用的时候,需要从其他地方拿到这个(回调)函数,以参数的形式传入.一般的函数调用,可以称作是调用.然而另一种,执行时才将某个函数传入再调用的调用方式,就叫"回调",当然,不要纠结于翻译的准不准,主要需要理解本质是什么.
4. 类基础
这个小节介绍了类的基本设计原理。
basic points
-
类:定义一个类,本质上是定义一个数据类型的蓝图。这实际上并没有定义任何数据,但它定义了类的名称意味着什么,也就是说,它定义了类的对象包括了什么,以及可以在这个对象上执行哪些操作。
-
成员函数通过一个名为this的隐式额外参数来访问调用它的对象。this参数是一个常量指针,被初始化为调用该函数的对象地址。在函数体内可以显式使用this指针。形参表后面的const,改变了隐含的this形参的类型,如 bool same_isbn(const Sales_item &rhs) const,这种函数称为“常量成员函数”(this指向的当前对象是常量)。
-
友元的声明以关键字 friend开始。 friend Sales_data add(const Sales_data&, const Sales_data&);表示非成员函数add可以访问类的非公有成员。
-
构造函数分为带参数和不带参数的,带参数可以实现不同对象不同初始化。而且构造函数可以有多个,做重载。
-
析构函数是函数内调用结束会释放, 但是static类型是不释放, 等main函数完了之后才调用。全局类也是这样。析构函数一个是为了存放资源,另外是为了执行用户希望最后一次对象执行的操作。许多简单的类中一般是默认的类,因为没必要特殊设计析构函数。
-
类的其他特性:定义在类内部的函数是自动内联的。在类外部定义的成员函数,也可以在声明时显式地加上 inline。
-
静态成员函数是为了处理静态成员变量, 因为静态成员变量没有this指针。(如果强行弄也可以,但是会降低代码可读性。)
-
static修饰的成员变量和成员函数属于类,而不属于对象。要用类来调用,而且所有对象共享他们内存。这对我们想把一些类的变量设置成全对象共有是非常有帮助的。虽然全局的变量也可以,但是影响封装性和代码可控性。 static 成员只能调用类的成员变量(也就是static)。 通常不在类的内部初始化,而是在定义时进行初始化,如 double Account::interestRate = initRate();
设计技巧
- 使用成员访问限定符时候一个限定符尽量只写一次,而且public写在前面,可以让其他开发人员一眼关注到能被外界调用的类。
- 为了保证封装性, 我们一般是类声明放在.h , 类的成员实现放在另外一个文件, 这样可以做到接口和实现分离。接口是指可以访问到的成员, 实现是指整个类的实现。 我们说的接口和实现分离就是为了让开发者调用类希望展示出来的接口。
- 通常情况下,我们都会设置类成员状态为私有(private),除非我们真的需要将其暴露, 这样做二次维护的时候不需要修改类外部的代码, 很方便。
- 什么时候用类,什么时候用结构体呢? 首先我觉得你要明确自己需求,如果只是表达确定的数据,没有抽象继承的需求, 就可以用结构体。
- 友元函数会破坏类的封装性,因此不建议使用。
- 构造函数里面就放初始化的东西,不提倡放与初始化无关的内容。
- 最好让构造函数初始值的顺序和成员声明的顺序保持一致。
面试题
Q1. 分别介绍一下类的三大属性?
- 封装继承多态。 封装是让类能够隐藏的内容都封装到类的内部。 继承是为了让类的重用性更高。 多态是实现同一种方法的不同操作。接口是通过多态实现的,通过多态去让不同类能实现通用的处理。 例如界面ui类,虽然每个类属性不同,但是通过多态实现这些接口之后, 不管哪个类了,直接调用就行,做成了通用接口。 以后只要看到不同类调用同一个函数显示不同的界面就是多态接口。 如果是不同对象放入同一个函数就是函数重载或者模板。
Q: public/protected/private的区别?
-
public的变量和函数在类的内部外部都可以访问。
-
protected的变量和函数只能在类的内部和其派生类中访问。
-
private修饰的元素只能在类内访问。
Q2 : 类和结构体的区别?
- 类和结构区别是: 首先struct 默认访问是public, class默认访问是private.
-
- 在c语言中struct是不能写function, 但是c++语言中可以。
Q3: 结构体内存对齐问题?
-
结构体内成员按照声明顺序存储,第一个成员地址和整个结构体地址相同。
-
未特殊说明时,按结构体中size最大的成员对齐(若有double成员),按8字节对齐。
Q : 对象存储空间? -
非静态成员的数据类型大小之和。
-
编译器加入的额外成员变量(如指向虚函数表的指针)。
-
为了边缘对齐优化加入的panding。
Q: 友元函数定义?
- 友元提供了不同类的成员函数之间、类的成员函数与一般函数之间进行数据共享的机制。通过友元,一个不同函数或另一个类中的成员函数可以访问类中的私有成员和保护成员。应用有操作符重载
Q: static成员 函数和成员变量的作用和定义
- static 作用于类的成员变量和类的成员函数,使得类变量或者类成员函数和类有关,也就是说可以不定义类的对象就可以通过类访问这些静态成员。注意:类的静态成员函数中只能访问静态成员变量或者静态成员函数,不能将静态成员函数定义成虚函数。
Q: 类的内存分布,空类的大小
- 遵循结构体的对齐原则。
与普通成员变量有关,与成员函数和静态成员无关。即普通成员函数,静态成员函数,静态数据成员,静态常量数据成员均对类的大小无影响。因为静态数据成员被类的对象共享,并不属于哪个具体的对象。
虚函数对类的大小有影响,是因为虚函数表指针的影响。
虚继承对类的大小有影响,是因为虚基表指针带来的影响。
空类的大小是一个特殊情况,空类的大小为 1,当用 new 来创建一个空类的对象时,为了保证不同对象的地址不同,空类也占用存储空间。
Q this 指针
- 常量指针
#include <iostream>
#include <cstring>
using namespace std;
class A
{
public:
void set_name(string tmp)
{
this->name = tmp;
}
void set_age(int tmp)
{
this->age = age;
}
void set_sex(int tmp)
{
this->sex = tmp;
}
void show()
{
cout << "Name: " << this->name << endl;
cout << "Age: " << this->age << endl;
cout << "Sex: " << this->sex << endl;
}
private:
string name;
int age;
int sex;
};
int main()
{
A *p = new A();
p->set_name("Alice");
p->set_age(16);
p->set_sex(1);
p->show();
return 0;
}
5. 类的高级设计
包含了 拷贝控制,运算符重载与类型转换,模板类, 类的继承,多态等。 让我们掌握如何依据基本的类的特性去设计一个好得类。
basic knowledge
- 一个类通过定义五种特殊的成员函数来控制对象的拷贝、移动、赋值和销毁操作。
拷贝构造函数(copy constructor) : 如果类未定义自己的拷贝构造函数,编译器会为类合成一个。一般情况下,合成拷贝构造函数(synthesized copy constructor)会将其参数的非static成员逐个拷贝到正在创建的对象中。
class Foo{
pubic:
Foo();
Foo(const Foo &);
}
Foo a;
b = Foo(a);
拷贝赋值运算符(copy-assignment operator)
class Foo{
pubic:
Foo();
Foo& operator=(const Foo&);
}
Foo a;
b = a ;
移动构造函数(move constructor) : 通过右值引用实现的。
移动赋值运算符(move-assignment operator) 通过右值引用实现的。
析构函数(destructor) :析构函数负责释放对象使用的资源,并销毁对象的非static数据成员。析构函数的名字由波浪号~接类名构成,它没有返回值,也不接受参数。
- 右值引用只能绑定到即将被销毁,并且没有其他用户的临时对象上。使用右值引用的代码可以自由地接管所引用对象的资源。
- default 关键字用于构造析构的默认值设置。
- 运算符重载过程中,当一个重载的运算符是成员函数时,this绑定到左侧运算对象。动态运算符符函数的参数数量比运算对象的数量少一个。但是赋值(=)、下标([])、调用(())和成员访问箭头(->)运算符必须是成员。而且有的符号是不能进行重载的。
- C++ 允许在同一作用域中的某个函数和运算符指定多个定义,分别称为函数重载和运算符重载。声明加法运算符用于把两个 Box 对象相加,返回最终的 Box 对象。大多数的重载运算符可被定义为普通的非成员函数或者被定义为类成员函数。如果我们定义上面的函数为类的非成员函数,那么我们需要为每次操作传递两个参数,如下所示:
Box operator+(const Box&, const Box&);
注意有些运算符是不能重载的。
-
类型转换运算符是类的一种特殊成员函数,它负责将一个类类型的值转换成其他类型。类型转换函数的一般形式如下:operator type() const;
-
类模板大大提高编程的效率, 给出一套代码,可以生成各种类。 虽然功能相同,但是可以处理的数据不一样。
-
继承过程中,允许我们依据另一个类来定义一个类,达到了重用代码功能和提高执行效率的效果。派生类可以访问基类中所有的非私有。 无论是基类还是子类,对象是不能访问protected成员的,而类的成员函数(基类或子类)都是可以访问protected成员的。
-
一个派生继承了所有的基类方法,除了基类的构造函数、析构函数和拷贝构造函数, 基类的重载运算符和友元函数。派生类的构造函数要对基类的成员,新增的数据成员和自己包含的对象成员进行构造。
-
虚函数 是在基类中使用关键字 virtual 声明的函数。在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数。 纯虚函数则不能够在基类定义,派生类里面去实现。虚函数还可以实现使用基类指针去调用各个派生类中的同名函数。如果不是虚函数则不能通过基类去调用。析构函数可以定义为虚函数,这样可以避免内存泄漏。(用基类指针去释放派生类时候会发生)
-
如果派生类在虚函数声明时使用了override描述符,那么该函数必须重载其基类中的同名函数,否则代码将无法通过编译。
-
final 代表这个类不能被继承。
-
带纯虚函数的类叫抽象类,这种类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。抽象类被继承后,子类可以继续是抽象类,也可以是普通类。只含有纯虚函数的类叫做接口类。
-
类成员函数和构造析构函数是不占用对象的空间的, 他们都是公共的地址。 但是虚函数是占用的。
-
C++11中,在类名后面添加final关键字可以禁止其他类继承它。
-
虚函数指针:在含有虚函数类的对象中,指向虚函数表,在运行时确定。
虚函数表:在程序只读数据段(.rodata section,见:目标文件存储结构),存放虚函数指针,如果派生类实现了基类的某个虚函数,则在虚表中覆盖原本基类的那个虚函数指针,在编译时根据类的声明创建。 -
虚继承是解决二次继承问题的。 虚继承底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。
实际上,vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。
设计技巧
- 设计基类用于派生的时候要慎重选择, 如果出现一些成员后面用不到, 会造成一层层的浪费。
- 继承是破坏封装性的,因此继承在实际开发中要慎用,遇到多种交互问题优先使用组合而不是继承。 多继承就不用说了,更乱。 很多时候这些继承是组合代码开发完成之后重构的结果, 而不是刚开始开发就用的。
- 多继承即一个子类可以有多个父类,它继承了多个父类的特性。但是不建议使用,很多语言都把这种方式砍掉了,因为比较复杂,影响代码的重构性。
- 基类通常都应该定义一个虚析构函数,即使该函数不执行任何实际操作也是如此。
面试题
Q: C++空类有哪些成员函数?
- 首先,空类大小为1字节。
- 默认函数有:构造函数 析构函数 拷贝构造函数 赋值运算符
Q: 构造函数调用顺序,析构函数呢?
- 基类的构造函数:如果有多个基类,先调用纵向上最上层基类构造函数,如果横向继承了多个类,调用顺序为派生表从左到右顺序。
成员类对象的构造函数:如果类的变量中包含其他类(类的组合),需要在调用本类构造函数前先调用成员类对象的构造函数,调用顺序遵照在类中被声明的顺序。
派生类的构造函数。
析构函数与之相反。
Q: 哪几种情况必须用到初始化成员列表{}?
-
初始化一个const成员。
-
初始化一个reference成员。
-
调用一个基类的构造函数,而该函数有一组参数。
-
调用一个数据成员对象的构造函数,而该函数有一组参数。
Q : 右值引用的作用? -
c++11中 右值引用是用来支持转移语义的。转移语义可以将资源 ( 堆,系统对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。临时对象的维护 ( 创建和销毁 ) 对性能有严重影响。
#include <iostream>
using namespace std;
int g_constructCount=0;
int g_copyConstructCount=0;
int g_destructCount=0;
struct A
{
A(){
cout<<"construct: "<<++g_constructCount<<endl;
}
A(const A& a)
{
cout<<"copy construct: "<<++g_copyConstructCount <<endl;
}
~A()
{
cout<<"destruct: "<<++g_destructCount<<endl;
}
};
A GetA()
{
return A();
}
int main() {
A a = GetA();
return 0;
}
例如上面的 A a 如果是A &&a 就会让 return A();这个临时变量延长生命周期, a拿到这个临时变量的引用。
Q2: 浅拷贝和深拷贝的区别是什么?
- 浅拷贝:重新在堆中创建内存,拷贝前后对象的基本数据类型互不影响,但拷贝前后对象的引用类型因共享同一块内存,会相互影响。 深拷贝:从堆内存中开辟一个新的区域存放新对象,对对象中的子对象进行递归拷贝,拷贝前后的两个对象互不影响。有指针的地方就有浅拷贝和深拷贝。
Q3. 动态多态和静态多态的区别?
- 动态多态是在运行时实现的, 他是通过virtual 。 静态多态在编译时候实现,通过重载。
Q4: 虚析构函数的作用?
- 虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。例如如下, 先调用派生类,才会调用基类。
class Shape
{
public:
Shape(); // 构造函数不能是虚函数
virtual double calcArea();
virtual ~Shape(); // 虚析构函数
};
class Circle : public Shape // 圆形类
{
public:
virtual double calcArea();
...
};
int main()
{
Shape * shape1 = new Circle(4.0);
shape1->calcArea();
delete shape1; // 因为Shape有虚析构函数,所以delete释放内存时,先调用子类析构函数,再调用基类析构函数,防止内存泄漏。
shape1 = NULL;
return 0;
}
Q5: 虚函数指针和虚函数表内存如何分配?
虚表是和类对应的,虚表指针是和对象对应的。编译时若基类中有虚函数,编译器为该的类创建一个一维数组的虚表,存放是每个虚函数的地址。基类和派生类都包含虚函数时,这两个类都建立一个虚表。构造函数中进行虚表的创建和虚表指针的初始化。在构造子类对象时,要先调用父类的构造函数,初始化父类对象的虚表指针,该虚表指针指向父类的虚表。执行子类的构造函数时,子类对象的虚表指针被初始化,指向自身的虚表。每一个类都有虚表。虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。
总结:
- 每一个派生类都有虚表。
- 虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类有3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。
- 派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。
补充: 关于具体的内存地址变化可以参照here
Q7 : 虚继承和虚函数的区别?
- 在这里我们可以对比虚函数的实现原理:他们有相似之处,都利用了虚指针(均占用类的存储空间)和虚表(均不占用类的存储空间)。
虚基类依旧存在继承类中,只占用存储空间;虚函数不占用存储空间。
虚基类表存储的是虚基类相对直接继承类的偏移;而虚函数表存储的是虚函数地址。
Q : 多重继承时会出现什么状况?如何解决?
- 使用虚继承的目的:保证存在命名冲突的成员变量在派生类中只保留一份,即使间接基类中的成员在派生类中只保留一份。在菱形继承关系中,间接基类称为虚基类,直接基类和间接基类之间的继承关系称为虚继承。
6. c++11
面试题
Q2 : 智能指针的作用?
- 智能指针的作用是管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。
- 2 c++11 中常用的智能指针有三个。shared_ptr, weak_ptr, unique_ptr .
unique_ptr<string> p3 (new string ("auto")); //#4
unique_ptr<string> p4; //#5
p4 = p3;//此时会报错!!
shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,
该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就
可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。
可以通过成员函数use_count()来查看资源的所有者个数。除了可以
通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。
当我们调用release()时,当前指针会释放资源所有权,计数减一。
当计数等于0时,资源会被释放。
weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr
相互引用,那么这两个指针的引用计数永远不可能下降为0,
资源永远不会释放。它是对对象的一种弱引用,
不会增加对象的引用计数,和shared_ptr之间可以相互转化,
shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。
Q: 智能变量auto
多用来迭代器的前缀自动分析 for(auto it = vec.begin(); it != vec.end();++it){…}
Q: 标准库 move() 函数
- std::move 可以将一个左值强制转化为右值,继而可以通过右值引用使用该值,以用于移动语义。
Q default 定义 - default 表示编译器生成默认的函数,例如:生成默认的构造函数。
Q delete 函数:
- delete 表示该函数不能被调用。
Q: 说一说c++11中四种cast转换?
- static_cast, dynamic_cast, const_cast, reinterpret_cast
- 1、const_cast用于将const变量转为非const
2、static_cast用于各种隐式转换,比如非const转const,void*转指针等, static_cast能用于多态向上转化,如果向下转能成功但是不安全,结果未知;
3、dynamic_cast用于动态类型转换。只能用于含有虚函数的类,用于类层次间的向上和向下转化。只能转指针或引用。向下转化时,如果是非法的对于指针返回NULL,对于引用抛异常。要深入了解内部转换的原理。它通过判断在执行到该语句的时候变量的运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
4、reinterpret_cast几乎什么都可以转,比如将int转指针,可能会出问题,尽量少用;
5、为什么不使用C的强制转换?C的强制转换表面上看起来功能强大什么都能转,但是转化不够明确,不能进行错误检查,容易出错。
3. c++标准库
STL大体分为六大组件,分别是:容器、算法、迭代器、仿函数、适配器(配接器)、空间配置器。
容器:各种数据结构,如vector、list、deque、set、map等,用来存放数据。
算法:各种常用的算法,如sort、find、copy、for_each等
迭代器:扮演了容器与算法之间的胶合剂。 提供一种方法,使之能够依序寻访某个容器所含的各个元素,而又无需暴露该容器的内部表示方式。
仿函数:行为类似函数,可作为算法的某种策略。
适配器:一种用来修饰容器或者仿函数或迭代器接口的东西。
空间配置器:负责空间的配置与管理。 我们可以把这些容器看成python的基本类型, 这样的话使用时候就会有很多通用的思路。
基础知识全部查看黑马的blog
1. IO标准库
主要介绍了系统级别的标准库如何使用。这里就不单独列出了。
2. 顺序容器
容器的接口都是一样的, 掌握一个容器的使用,自然也掌握了其他容器的用法。顺序容器控制了元素的存储和访问顺序。
设计技巧
- 容器选择原则:
- 除非有合适的理由选择其他容器,否则应该使用
vector
。 - 如果程序有很多小的元素,且空间的额外开销很重要,则不要使用
list
或forward_list
。 - 如果程序要求随机访问容器元素,则应该使用
vector
或deque
。 - 如果程序需要在容器头尾位置插入/删除元素,但不会在中间位置操作,则应该使用
deque
。 - 如果程序只有在读取输入时才需要在容器中间位置插入元素,之后需要随机访问元素。则:
- 先确定是否真的需要在容器中间位置插入元素。当处理输入数据时,可以先向
vector
追加数据,再调用标准库的sort
函数重排元素,从而避免在中间位置添加元素。 - 如果必须在中间位置插入元素,可以在输入阶段使用
list
。输入完成后将list
中的内容拷贝到vector
中。
- 先确定是否真的需要在容器中间位置插入元素。当处理输入数据时,可以先向
- 不确定应该使用哪种容器时,可以先只使用
vector
和list
的公共操作:使用迭代器,不使用下标操作,避免随机访问。这样在必要时选择vector
或list
都很方便。
- 当我们使用一次insert和erase 时候都会返回一个新的迭代器用来访问新的容器, 因此一定注意这里面容易产生bug。
面试题
Q1 : 请你分析一下vector容器的复杂度?
查找的复杂度为1 , 末尾插入和删除的复杂度也为1 。 但是中间插入删除的复杂度为n。 不如有些顺序容器方便。
Q1: 请你实现一个string或者vector.
A: 已经实现, 做的时候要注意获取大小之后进行扩容, 然后把之前的数据全部放进去。
Q2: array 和vector 区别?
- vector属于变长容器,即可以根据数据的插入删除重新构建容器容量;但array和数组属于定长容量。
- vector和array提供了更好的数据访问机制,即可以使用front和back以及at访问方式,使得访问更加安全。而数组只能通过下标访问,在程序的设计过程中,更容易引发访问 错误。
- vector和array提供了更好的遍历机制,即有正向迭代器和反向迭代器两种。
Q : 哪些情况下迭代器会失效?
插入元素时 : 尾后插入:size < capacity时,首迭代器不失效尾迭代实现(未重新分配空间),size == capacity时,所有迭代器均失效(需要重新分配空间)。
删除元素尾后时候只有尾迭代失效。中间删除时候删除位置之后所有迭代失效。
Q: 一种抽象的设计概念,在设计模式中有迭代器模式,即提供一种方法,使之能够依序寻访某个容器所含的各个元素,而无需暴露该容器的内部表述方式。作用:在无需知道容器底层原理的情况下,遍历容器中的元素。
3. 关联和非关联容器
面试题
Q1 : map和set是用什么实现的? 为什么要这样做?
- 使用红黑树实现的,这样会自动排序。 然后我们去查找插入删除的时候就会非常快。map中所有元素都是pair, pair中第一个元素为key(键值),起到索引作用,第二个元素为value(实值)所有元素都会根据元素的键值自动排序。 map 是基于红黑树实现的map结构(实际上是map, set, multimap,multiset底层均是红黑树),不仅增删数据时不需要移动数据,其所有操作都可以在O(logn)时间范围内完成。另外,基于红黑树的map在通过迭代器遍历时,得到的是key按序排列后的结果,这点特性在很多操作中非常方便。RBTree本身也是二叉排序树的一种,key值有序,且唯一。必须保证key可排序。
- set所有元素都会在插入时自动被排序. set/multiset属于关联式容器,底层结构是用二叉树实现。 set不允许容器中有重复的元素 .multiset允许容器中有重复的元素. 对于自定义数据类型,set必须指定排序规则才可以插入数据.
Q: 非关联容器的定义?
哈希函数存储的。
4 泛型算法
标准库定义的容器操作非常少,但是标准库提供了一组泛型算法可以对不同的容器进行操作。
basic points
- 泛型算法:因为它们实现共同的操作,所以称之为“算法”;而“泛型”、指的是它们可以操作在多种容器类型上。泛型算法本身不执行容器操作,只是单独依赖迭代器和迭代器操作实现。
- 模板是泛型编程的基础,泛型编程即以一种独立于任何特定类型的方式编写代码。库容器,比如迭代器和算法,都是泛型编程的例子,它们都使用了模板的概念。如 vector 或 vector 。
- 大多数算法都定义在头文件algorithm中,此外标准库还在头文件numeric中定义了一组数值泛型算法。一般情况下,这些算法并不直接操作容器,而是遍历由两个迭代器指定的元素范围进行操作。
- find algorithm :vector::const_iterator result = find(vec.begin(), vec.end(), search_value);
- accumulate 算法: 只读取范围中的元素,不改变元素。
- 写容器元素的算法: fill: fill(vec.begin(), vec.end(), 0); 将每个元素重置为0
- 重排容器元素的算法sort:接受两个迭代器,表示要排序的元素范围。
- 特定容器算法 :对于list和forward_list,优先使用成员函数版本的算法而不是通用算法。
4. 整体设计方法
- c++编程风格结合《Effective C++》和《Effective STL》可以让自己写出高质量的代码。不然有些欠的债是要自己后面去还的。
- C标准库是在操作系统API上加入独特的算法封装成标准接口的库,使用C标准库可以屏蔽底层实现细节,比如fopen这样的函数,在Windows上通过调用CreateFileEx实现,在linux上通过调用open系统调用实现。不仅是包装,还在上层使用独特的算法提供了用户态缓冲区的功能。但是早起的c++封装并不完整,不像java封装的那么多。 c++是一个不完全封装的语言,它不像java, jdk里面封装了函数能够对操作系统底层api进行操作, 早期的c++stl对操作系统支持并不好, 因此对于一些比较特别的任务还要去学开发系统平台原生的一些API,因为不同的系统底层api不一样,c++可能标准库里面没有同意封装这个功能。
- 为什么要会多线程和多进程?在只有一个核的CPU时代,程序员写的程序不需要修改,就可以通过换成更高主频的CPU而获得性能的提升。但是,在多核CPU时代,如果程序写的程序只有一个线程,换成2核,4核,8核的CPU,程序的性能无法获得增长,于是就要求程序员写多线程的程序,将计算问题分解到多个线程上解决,这样才能获得性能的提升。当多任务操作系统运行在单CPU的计算机上,多个线程/进程是轮流按照时间片共享单个CPU的。当多任务操作系统运行在对称对处理器/核的计算机上,多个进程会被调度到不同的核心上运行。
reference
100 -104