C++(基础)笔记与总结
前言
你好,我是苏青羽,是一名默默无闻的计算机爱好者。
本笔记是我在学习C++时所总结的学习笔记,主要记录了C++的一些基础知识点,在这里分享给大家,希望通过该笔记能帮助更多的人去理解C++。
笔记中参考了网上的很多博客、公众号、网课、面经等等,这些均在笔记末尾的参考资料中有所展示,大家可自行查看,做更深入的了解。
本笔记主要用于在学习上的复盘,并不适合初学者学习。如果在笔记中遇到了不懂的知识点,一定要去自己看书或者上网查询相关知识点!
如果发现本文有较大的硬性错误、或者是某些内容存在侵权、以及有什么想补充的问题和内容,请点击“关于”,通过里面预留的联系方式同我联系!
本笔记正在实时更新中,若是想获取笔记的最新PDF版以及了解关于我的更多信息,可扫描下方二维码,或微信搜索公众号“苏青羽”关注我!
前提基础
学习本笔记前,请事先掌握以下基础知识:
- C++基本语法
- C++标准模版库(STL)
- 数据结构与算法
C++98
1. 在main执行的前后需要处理什么工作?
Main函数之前
(1) 设置栈指针
(2) 全局对象初始化,在main之前调用构造函数
(3) 将main函数的参数argc,argv等传递给main函数
Main函数之后
全局对象的析构函数
2. 什么是内存对齐?
(1) 在计算机中,内存是按字节划分的,而CPU在读取数据时,并不是一个字节一个字节的读取,实际上是按块的大小读取,块大小可以是2,4,8,16等等,称为内存访问粒度。
(2) 内存对齐则是将特定的数据类型按照一定的规则摆放在内存上,具体规则是按照变量的声明顺序,依次安排内存,其偏移量为变量大小的整数倍。
3. 为什么要做内存对齐?
(1) 平台原因(移植原因):不是所有的硬件平台都能访问任意地址上的任意数据的;某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。
(2) 性能原因:数据结构(尤其是栈)应该尽可能地在自然边界上对齐。原因在于,为了访问未对齐的内存,处理器需要作两次内存访问;而对齐的内存访问仅需要一次访问。
4. 说说内存对齐规则?
(1) 每个特定平台上的编译器都有自己的默认“对齐系数”(也叫对齐模数)。32位系统,gcc中默认#pragma pack(4),即对齐系数默认为4。可以通过预编译命令#pragma pack(n),n = 1,2,4,8,16来改变这一系数。
(2) 有效对齐值:是给定值#pragma pack(n)和结构体中最长数据类型长度中较小的那个。有效对齐值也叫对齐单位。
(3) 结构体第一个成员的偏移量为0,以后每个成员相对于结构体首地址的偏移量都是该成员大小与有效对齐值中较小数的整数倍,如有需要编译器会在成员之间加上填充字节。
(4) 结构体的总大小为有效对齐值的整数倍,如有需要编译器会在最末一个成员之后加上填充字节。
5. 指针和引用的区别?
(1) 指针是一个变量,存储的是一个地址。引用跟原来的变量实质上是同一个东西,是原变量的别名
(2) 指针可以有多级,引用只有一级
(3) 指针可以为空,引用不能为NULL且在定义时必须初始化
(4) 指针在初始化后可以改变指向,而引用在初始化之后不可再改变
(5) sizeof指针得到的是本指针的大小,sizeof引用得到的是引用所指向变量的大小
6. 堆和栈的区别?
(1) 栈由系统自动分配,堆是自己申请和释放的。
(2) 堆向上,向高地址方向增长。栈向下,向低地址方向增长。
(3) 在空间连续性上,栈区的空间是连续的;但堆的空间是不连续的。
(4) 堆只能动态分配。栈有静态分配和动态分配,静态分配由编译器完成,动态分配由alloca函数分配,但栈的动态分配的资源由编译器进行释放,无需程序员实现。
(5) 因为操作系统会在底层对栈提供支持,会分配专门的寄存器存放栈的地址,栈的入栈出栈操作也十分简单,并且有专门的指令执行,所以栈的效率比较高也比较快。
(6) 堆的操作是由C/C++函数库提供的,在分配堆内存的时候需要一定的算法寻找合适大小的内存。并且获取堆的内容需要两次访问,第一次访问指针,第二次根据指针保存的地址访问内存,因此堆比较慢。
7. new / delete 与 malloc / free的异同?
(1) 都可用于内存的动态申请和释放
(2) 前者是C++运算符,后者是C/C++语言标准库函数
(3) new自动计算要分配的空间大小,malloc需要手工计算
(4) new是类型安全的,malloc不是。
(5) new的实现过程是:首先调用名为operator new的标准库函数,分配足够大的原始类型的内存,接下来在该内存上调用构造函数初始化对象;最后返回该内存指针。
(6) delete的实现过程:对指针指向的对象运行适当的析构函数;然后通过调用名为operator delete的标准库函数释放该对象所用内存
(7) malloc/free可重载,new/delete不可重载,operator new/operator delete可重载
8. 被free回收的内存是立即返还给操作系统吗?
不会,new或者malloc申请内存是向内存管理器申请,内存管理器再向操作系统申请,这里面涉及到系统调用,如果频繁的申请释放,效率会很低,所以一般进程申请了内存后,释放资源后并不会立即将内存还给操作系统,而是放到一个类似于内存缓存池的地方,下次申请的时候首先会在内存缓存池中查找合适的内存,减少了大量的系统调用,提高速度。
9. 宏函数和普通函数有何区别?
(1) 宏函数作用在预编译期,进行文本替换,相当于直接插入了代码,运行时不存在函数调用,执行起来更快;普通函数调用在运行时需要跳转到具体调用函数。
(2) 宏函数属于在结构中插入代码,没有返回值;普通函数调用具有返回值。
(3) 宏函数参数没有类型,不进行类型检查;普通函数参数具有类型,需要检查类型。
10. 宏定义和typedef有何区别?
(1) 宏主要用于定义常量及书写复杂的内容;typedef主要用于定义类型别名。
(2) 宏替换发生在预编译期,属于文本插入替换;typedef是编译的一部分。
(3) 宏不检查类型;typedef会检查数据类型。
11. 宏定义和const的区别?
(1) 宏定义发生在预编译期。const是在编译、运行的时候起作用
(2) 宏定义只做替换,不做类型检查和计算。const常量有数据类型,编译器做类型检查。
(3) define只是将宏名称进行文本替换,占用代码段内存。const在程序运行中只有一份备份,占用数据段内存。
12. 内联函数和宏函数的区别?
(1) 宏函数在预处理阶段进行文本替换,inline函数在编译阶段进行替换
(2) inline函数有类型检查,相比宏函数比较安全
(3) Inline函数具有返回值,宏函数没有
13. 变量声明和定义的区别?
声明仅仅是把变量的声明的位置及类型提供给编译器,并不分配内存空间。只有在定义处才为其分配存储空间。相同变量可以在多处声明,但只能在一处定义
14. strlen和sizeof的区别?
(1) sizeof是C/C++操作符,并不是函数,结果在编译时得到。strlen是字符处理的库函数。
(2) sizeof参数可以是任何数据的类型或者数据。strlen的参数只能是字符指针,它指向了一个结尾是’\0’的字符串。
15. 常量指针和指针常量的区别?
(1) 指针常量(底层const)是一个指针,它指向了一个只读值。它实际上只是限制了指针的拥有者不能通过指针修改它所指向的值,对这个值的属性没有限制,这个值是可以是常量也可以变量,均不影响。
(2) 常量指针(顶层const)是一个不能给改变指向的指针,即我们可以通过指针修改它所指向的值,但不能修改指针的指向。
16. 指针常量能不能赋值给非指针常量?
不能,指针常量限制了指针的拥有者不能通过指针修改它所指向的值,如果指针常量可以赋值给非指针常量,那就意味着拥有者可以使用这种方法获取写权限,这在语义上是冲突的。
17. C++和Python的区别?
(1) Python是一种脚本语言,是解释执行的,而C++是编译语言,是需要编译后在特定平台运行的。
(2) python可以很方便的跨平台,但是效率没有C++高。
(3) C++中需要事先定义变量的类型,而Python不需要
(4) Python的库函数比C++的多,调用起来很方便
18. C++中struct和class的区别?
(1) 两者都拥有成员函数、公有和私有部分
(2) 任何可以使用class完成的工作,同样可以使用struct完成
(3) 两者中如果不对成员不指定公私有,struct默认是公有的,class则默认是私有的
(4) class默认是private继承,而struct模式是public继承
19. C++和C的struct的区别?
C语言:struct是用户自定义数据类型(UDT),不能设置权限,成员不可以是函数,不能被继承
C++:struct是抽象数据类型(ADT)支持成员函数的定义,能设置权限,能被继承与实现多态。
20. C++中const和static的作用?
(1) Static(类外):
① 隐藏。所有不加static的全局变量和函数具有全局可见性,可以在其他模块中使用,加了之后只能在该文件所在的编译模块中使用
② 静态变量没有定义初始值时,会初始化为0。
③ 静态变量在函数内定义,生命周期跟随程序(同全局变量),且只进行一次初始化,具有记忆性,其作用范围与局部变量相同,函数退出后仍然存在,但不能使用
(2) Static(类内):
① static成员变量:只与类关联,不与类的对象关联。定义时要分配空间,不能在类声明中初始化, 必须在类定义体外部初始化,初始化时不需要标示为static;可以被非static成员函数任意访问。
② static成员函数:不具有this指针,无法访问类对象的非static成员变量和非static成员函数;不能被声明为const、虚函数和volatile;可以被非static成员函数任意访问
(3) Const(类外):
① const常量在定义时必须初始化,之后无法更改
② const形参可以接收const和非const类型的实参
(4) Const(类内):
① const成员变量:只能通过构造函数初始化列表进行初始化,并且必须有构造函数;不同类对其const数据成员的值可以不同,所以不能在类中声明时初始化
② const成员函数:const类对象不可以调用类中的非const成员函数;非const类对象则均可调用;const成员函数不可以改变类中非mutable数据成员的值。
21.数组名和指针(这里为指向数组首元素的指针)的区别?
(1) 编译器为了简化对数组的支持,实际上是利用指针实现了对数组的支持。具体来说,就是将表达式中的数组元素引用转换为指针加偏移量的引用。
(2) 二者均可通过增减偏移量来访问数组中的元素。
(3) 数组名不是真正意义上的指针,实际上是一个常量指针,所以不能进行自增自减操作。
(4) 当数组名当做形参传递给调用函数后,就失去了原有特性,退化成一般指针,多了自增、自减操作, 但sizeof运算符不能再得到原数组的大小了。
22. C代码使用C语言编译和C++编译有什么不同?
(1) C++虽然语法上支持和兼容C语言,但为了支持多种特性,在编译上却做了很多C语言所没有的其他处理。
(2) 比如说现在有一个C语言函数库,而我们用C++去调用该函数库,在编译链接时链接器会报错。因为C++为了支持函数重载,会在编译时给每个函数进行“改名”,但是C语言在编译时则不会改名,C函数库中的函数名都保持原样,这就会使链接器在函数库中找不到改名后的对应函数地址,然后报错。extern “C”可以很好地解决这个问题。
(3) 因为C++的函数改名规则,C++代码在使用其他模块的函数前必须包含其头文件或显式声明函数,不然它无法识别该函数是C函数还是C++函数,是否需要进行改名。C语言编译器即使不提前声明函数也可以调用函数,因为C编译器没有改名规则。
23. extern”C”的用法?
在程序中加上extern “C”后,相当于告诉编译器这部分代码是C语言写的,因此要按照C语言进行编译,而不是C++,在以下情况时会用到该语法:
(1) C++代码中调用C语言函数库;
(2) 在多个人协同开发时,可能有人擅长C语言,而有人擅长C++
24. 说说野指针和悬空指针?
(1) 野指针:指的是没有被初始化过的指针。解决方法:定义指针变量要及时初始化或者置空。(保持良好的编码规范)
(2) 悬空指针:指针最初指向的内存已经被释放了的一种指针。解决方法:释放操作后立即置空,或者使用智能指针
25. C++中的重载、重写(覆盖)和隐藏的区别?
(1) 重载是指在同一范围定义中的同名函数才存在重载关系。主要特点是函数名相同,参数类型和数目有所不同,不能出现参数个数和类型均相同,仅仅依靠返回值不同来区分的函数。
(2) 重写指的是在派生类中重写父类的函数体,要求基类函数必须是虚函数,且重写的函数签名(返回值、函数名、参数列表)必须完全一致。
(3) 隐藏指的是某些情况下,派生类中的函数屏蔽了基类中的同名函数。
(4) 重写是父类和子类之间的垂直关系,重载是不同函数之间的水平关系
(5) 隐藏和重写的区别在于基类函数是否是虚函数。
26. 浅拷贝和深拷贝的区别?
(1) 浅拷贝就是将对象的指针进行简单的复制,原对象和副本指向的是相同的资源。如果原来的指针所指向的资源释放了,那么使用新指针就会出现错误。
(2) 深拷贝是新开辟一块空间,将原对象的资源复制到新的空间中,并返回该空间的地址。即使原先的对象被析构掉,释放内存了也不会影响到深拷贝得到的值。
27. public,protected和private权限的区别?
(1) 访问权限:
① public的变量和函数在类的内部外部都可以访问。
② protected的变量和函数只能在类的内部和其派生类中访问。
③ private修饰的元素只能在类内访问。
(2) 继承权限:
① Public:基类的公有成员和保护成员作为派生类的成员时,都保持原有的访问权限,基类的私有成员任然是私有的
② Protected:基类的公有成员和保护成员都成为派生类的保护成员,基类的私有成员仍然是私有的
③ private:基类的公有成员和保护成员都成为派生类的私有成员,基类的私有成员任然是私有的
对于继承权限而言,它所针对的是类外的访问权限。对于类内来说,不管是何种继承,派生类均可访问基类的公用和保护成员,但不可访问基类的私有成员。
28. 如何用代码判断大小端存储?
(1) 大端存储:字数据的高字节存储在低地址中
(2) 小端存储:字数据的高字节存储在高地址中
(3) 方式一:使用强制类型转换
(4) 方式二:使用union联合体
29. volatile、mutable和explicit关键字的用法?
(1) volatile 关键字是一种类型修饰符,用它声明的类型变量表示它可能被某些编译器未知的因素更改,所以系统总是重新从它所在的内存读取数据而不会去使用内存读取优化(使用寄存器),即使它前面的指令刚刚从该处读取过数据。
(2) mutable关键字是一种类型修饰符,被mutable修饰的数据成员表示他可以被const成员函数所修改(const成员函数无法修改类中的普通数据成员),它的修改不会影响整个类对象的状态。
(3) explicit关键字用来修饰类的构造函数,加上该关键字,表示该类不能发生相应的隐式类型转换(函数调用传参时的类型转换),只能以显式地进行类型转换(调用构造函数) 。
30. C++中有几种类型的new?
在C++中,new有三种典型的使用方法:plain new,nothrow new和placement new
(1) plain new:最普通的new,它在分配内存失败时会抛出std::bad_alloc异常而不是返回NULL
(2) nothrow new :不会抛出异常的new,分配内存失败时返回NULL
(3) placement new:不分配内存,只在一块分配好了的内存上调用类的构造函数
31. 形参与实参的区别?
(1) 形参在定义时不分配内存,只有在被调用时才分配内存
(2) 实参可以任意形式的表达式,但在进行函数调用时,它们都必须具有确定的值
(3) 实参和形参在数量上,类型上,顺序上应严格一致,否则会发生“类型不匹配”的错误
(4) 函数调用中发生的数据传送是单向的。 即只能把实参的值传送给形参。
32. 值传递、指针传递、引用传递的区别和效率?
(1) 值传递:实参的值向形参进行值拷贝,如果值传递的对象是类对象或是大的结构体对象,将耗费一定的时间和空间。
(2) 指针传递:同样有实参的值向形参进行值拷贝,但拷贝的数据是一个固定大小的地址。
(3) 引用传递:同样有上述的数据拷贝过程,但其是针对地址的,相当于为该数据所在的地址起了一个别名。
(4) 效率上讲,指针传递和引用传递比值传递效率高。一般主张使用引用传递,代码逻辑上更加紧凑、清晰。
33. C++有哪几种的构造函数?
(1) 默认构造函数(无参数)
(2) 初始化构造函数(有参数)
(3) 拷贝构造函数
34. 什么情况下会调用拷贝构造函数?
(1) 用类的一个实例对象去初始化构造一个类的新对象的时候
(2) 函数的参数是类的对象时(值传递)
(3) 函数的返回值是函数体内局部对象的类的对象时
35. 说说C++中的初始化?
C++中变量的初始化有很多种方式,如:默认初始化,值初始化,直接初始化,拷贝初始化。下面一一介绍。
(1) 默认初始化
① 默认初始化是指定义变量时 没有指定初值时进行的初始化操作。
② 对于内置类型变量(如int,double,bool等),如果是全局变量或静态变量,则初始化为0,如果是栈或者堆变量,则将拥有未定义的值。
③ 对于类类型的变量(如string或其他自定义类型),不管定义于何处,都会执行默认构造函数。如果该类没有默认构造函数,则会引发错误。因此,建议为每个类都定义一个默认构造函数(=default)。
④ 注意:默认初始化的值并不是绝对的,在一些情况下会产生未知的错误,一般定义变量时最好都要为它设定初始值,这是一种良好的编程习惯。
(2) 值初始化
① 值初始化是指使用了初始化器(即使用了圆括号或花括号)但却没有提供初始值的情况。
② 特别的,采用动态分配内存的方式(即采用new关键字)创建的变量,不加括号时,如int *p=new int,是默认初始化,加了括号,如int *p=new int(),为值初始化。
③ 若不采用动态分配内存的方式(即不采用new运算符),写成int a();是错误的值初始化方式,因为这种方式是声明了一个函数而不是进行值初始化。如果一定要进行值初始化,必须结合拷贝初始化使用,即写成int a=int()。
④ 值初始化和默认初始化一样,对于内置类型初始化为0,对于类类型则调用其默认构造函数,如果没有默认构造函数,则不能进行初始化。
(3) 直接初始化
① 直接初始化是指采用小括号的方式进行变量初始化(小括号里一定要有初始值,如果没提供初始值,那就是值初始化了!)
② 对于类类型来说,直接初始化会直接调用与实参匹配的构造函数。
(4) 拷贝初始化
① 拷贝初始化是指采用等号(=)进行初始化的方式,编译器把等号右侧的初始值拷贝到新创建的对象中去。
② 拷贝初始化首先使用指定构造函数创建一个临时对象,然后用拷贝构造函数将那个临时对象拷贝到正在创建的对象。
36. 直接初始化、拷贝初始化、赋值的差别?
(1) 这三种操作非常类似,语法上也很相像,区分这三者主要看他们的最终调用的函数。
(2) 直接初始化:要创建的对象不存在,主要利用初始化器(圆括号)使用其他的初始值进行初始化操作,一般这个初始值是别的对象或数据类型,直接初始化调用类的构造函数(调用参数类型最佳匹配的那个)。
(3) 拷贝初始化:要创建的对象不存在,使用已有的对象(与创建的对象类型一致)来进行初始化,这个已有的对象既可以是临时对象也可以是其他对象。既可以使用初始化器(圆括号),也可以使用赋值号(“=”)来进行初始化操作,但他们的背后都会调用类的拷贝构造函数。
(4) 赋值:要创建的对象已存在,用已存在的对象给它赋值,这属于重载“=”号运算符的范畴,他并不是一种初始化操作,背后调用类的重载“=”运算符函数。
(5) 对于内置类型变量(如int,double,bool等),直接初始化与拷贝初始化差别可以忽略不计。
37. 静态变量什么时候初始化?
视编译器而定,有些可能在代码执行之前就初始化,有些则可能直到代码执行时才初始化。
38. delete、delete []的区别?
delete只会调用一次析构函数,delete [] 会根据数组元素的数量,对数组中的每个元素调用析构函数。
39. malloc、calloc、realloc的区别?
(1) malloc函数:void* malloc(unsigned int num_size); 需要手动计算分配大小,申请的空间的值是随机初始化的
(2) calloc函数:void* calloc(size_t n,size_t size);无需手动计算分配大小,申请的空间的值是初始化为0的
(3) realloc函数 :给动态分配的空间分配额外的空间,用于扩充容量。
40. 说说类成员的初始化方式?
(1) 构造函数初始化:在构造函数体中初始化,在所有的数据成员被分配内存空间后,才进行赋值操作。背后会调用一次数据成员的构造函数和赋值函数。
(2) 初始化列表:给数据成员分配内存空间时就进行初始化,相比起构造函数初始化,少了一次调用赋值函数的操作,因此效率会更高一些。
41. 构造函数的执行顺序?
(1) 基类的构造函数
(2) 派生类的数据成员的构造函数
(3) 派生类自己的构造函数
42. 析构函数的执行顺序?
(1) 调用派生类的析构函数;
(2) 调用派生类的数据成员的析构函数;
(3) 调用基类的析构函数。
43. 有哪些情况必须使用初始化列表?
(1) 当初始化一个引用成员时
(2) 当初始化一个常量成员时
(3) 当调用一个基类的构造函数,而它拥有一组参数时
(4) 当调用一个成员类的构造函数,而它拥有一组参数时
44. 初始化列表的初始化顺序?
初始化列表中出现的顺序并不是真正的初始化顺序,初始化顺序只取决于成员变量在类中的声明顺序。我们应尽可能保证成员变量的声明顺序与初始化列表顺序一致,才能真正保证其效率。
45. C++中新增的string与C中的 char *有什么区别?
(1) string对char*进行了封装,包含了字符串的属性以及对外提供了通用方法。
(2) string可以进行动态扩展,在每次扩展的时候另外申请一块原空间大小两倍的空间(2^n),然后将原字符串拷贝过去,并加上新增的内容。
46. 什么是内存泄露,如何检测与避免?
(1) 内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的内存块,使用完后必须显式释放的内存,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。
(2) 检测方法:有专门的内存泄漏检测工具,Linux下可以使用Valgrind工具 ,Windows下可以使用CRT库。
(3) 避免方法:使用智能指针,良好的编程习惯。
47. 什么是内存溢出和内存越界?
(1) 内存溢出指的是程序在申请内存时,没有足够的内存空间供其使用。
(2) 内存越界指的是申请了一块内存,使用的时候超出了这块内存区域。
48. 介绍C++面向对象的三大特性?
(1) 继承
让某个类获得另一个类的属性和方法。它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展,是代码复用的一种机制。
常见的继承有两种方式:
实现继承:指使用基类的属性和方法而无需额外编码的能力
接口继承:指仅使用属性和方法的名称、但是子类必须提供实现的能力
(2) 封装
数据和代码捆绑在一起,避免外界干扰和不确定性访问。在C++中借以权限机制实现这一特性,利用三种访问权限控制外界对对象数据的访问。
(3) 多态
多态是同一个行为具有多个不同表现形式或形态的能力。
常见的多态有两种方式:
编译时多态(重载):是指允许存在多个同名函数,而这些函数的参数表不同
运行时多态(重写):是指子类重新定义父类的虚函数的做法。
49. 说说C++的四种强制转换?
(1) reinterpret_cast:reinterpret_cast 用以处理互不相关的类型之间的转换,reinterpret_cast 操作执行的是比特位拷贝,即编译器不会做任何检查,截断,补齐的操作,只是把比特位拷贝过去。这种转换提供了很强的灵活性,但转换的安全性只能由程序员的细心来保证了。
(2) const_cast:该运算符用来修改类型的const属性,可以使指向常量的指针被转化成指向非常量的指针,并且仍然指向原来的对象,使拥有者可以通过指针修改对象。
(3) static_cast:static_cast 用于进行比较“自然”和低风险的转换,如整型和浮点型、字符型与整型之间的互相转换。static_cast 不能用于在不同类型的指针之间互相转换,也不能用于整型和指针之间的互相转换,当然也不能用于不同类型的引用之间的转换。因为这些属于风险比较高的转换。他的安全性比起reinterpret_cast 更高,但是由于该操作符没有运行时类型检查机制,在进行下行转换(把基类指针或引用转换成派生类指针或引用)时仍然是不安全的。
(4) dynamic_cast:dynamic_cast主要用于类层次间的上行转换和下行转换,因为有动态类型检测,在进行下行转换时比static_cast更安全。
50. 如何获得结构成员相对于结构开头的字节偏移量?
使用<stddef.h>头文件中的,offsetof宏
offsetof用法:offsetof(S, x),S为结构体对象,x为结构体数据成员之一
51. 静态类型和动态类型,静态绑定和动态绑定的介绍?
(1) 静态类型:对象在声明时采用的类型,在编译期既已确定;
(2) 动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;
(3) 静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在编译期;
(4) 动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型,发生在运行期;
52. 全局变量和局部变量有什么区别?
(1) 生命周期:全局变量随主程序创建和创建,随主程序销毁而销毁;局部变量在局部函数内部,甚至局部循环体等内部存在,退出就不存在;
(2) 作用域:通过声明后全局变量在程序的各个部分都可以用到;局部变量分配在堆栈区,只能在局部使用
53. 指针加减计算要注意什么?
指针加减本质是对其所指地址的移动,移动的步长跟指针的类型是有关系的,因此在涉及到指针加减运算需要十分小心,加多或者减多都会导致指针指向一块未知的内存地址,即内存越界。
54. 怎样判断两个浮点数是否相等?
对两个浮点数判断是否相等不能直接用==来判断,会出错!明明相等的两个数比较反而是不相等!对于两个浮点数比较只能通过相减并与预先设定的精度比较,记得要取绝对值!
55. 类如何实现只能静态分配和只能动态分配?
(1) 实现只能静态分配:把operator new运算符重载为private属性。
(2) 实现只能动态分配:把构造函数设为private属性
56. 知道C++中的组合吗?它与继承相比有什么优缺点吗?
继承有以下几个缺点:
(1) 父类的内部细节对子类是可见的,破坏了封装性。
(2) 子类从父类继承的方法在编译时就确定下来了,所以无法在运行期间改变从父类继承的方法的行为。
(3) 子类与父类是一种高耦合,违背了面向对象思想。
使用组合的目的即为了克服这几个缺点,但也因此产生了另一些缺点,如:容易产生过多的对象、为了能组合多个对象,必须仔细对接口进行定义。
57. 为什么模板类一般都是放在一个头文件中?
模板定义很特殊,编译器在遇到任何的模板定义时不会为它分配内存空间,它一直处于等待状态直到被一个模板实例告知才会分配内存空间。如果在分离式编译环境下,编译器编译某一个源文件时并不知道另一个源文件的存在,也不会去查找(当遇到未决符号时它会寄希望于连接器),同时由于模板定义没有被分配空间,链接器也无法查找到函数的入口地址。
58. 你了解重载运算符吗?
C++预定义中的运算符的操作对象只局限于基本的内置数据类型,但是对于我们自定义的类型(类)是没有办法操作的。但是大多时候我们需要对我们定义的类型进行类似的运算,这个时候就需要我们对这么运算符进行重新定义,赋予其新的功能,以满足自身的需求。这就是运算符重载,它的实质就是函数重载。
运算符重载规则:
(1) 为了防止用户对标准类型进行运算符重载,C++规定重载后的运算符的操作对象必须至少有一个是用户定义的类型。
(2) 使用运算符不能违法运算符原来的句法规则。如不能将‘+’重载为一个操作数。
(3) 不能修改运算符原先的优先级。
(4) 不能创建一个新的运算符
(5) 不能进行重载的运算符:成员运算符,作用域运算符,条件运算符,sizeof运算符,typeid运算符,const_cast、dynamic_cast、reinterpret_cast、static_cast强制类型转换运算符。
(6) 大多数运算符可以通过成员函数和非成员函数进行重载,但是下面这四种运算符只能通过成员函数进行重载:= 赋值运算符,()函数调用运算符,[ ]下标运算符,->通过指针访问类成员的运算符。
(7) 一般来说,单目运算符重载为类的成员函数,双目运算符重载为类的友元函数
59. 前++和后++重载的区别?
(1) 前++重载函数参数列表不需要带参数,后++参数列表需要带参数,这个参数仅仅只是区分前++和后++用,没有实际意义。
(2) 前++返回一个引用,后++返回一个临时对象,后++效率比较高。
60. 当程序中有函数重载时,函数的匹配原则和顺序是什么?
(1) 名字查找
(2) 确定候选函数
(3) 寻找最佳匹配
61. 条件编译的作用?
(1) 一般情况下,源程序中所有的行都参加编译。但是有时希望对其中一部分内容只在满足一定条件才进行编译,也就是对一部分内容指定编译的条件,这就是“条件编译”。
(2) 在一个大的软件工程里面,可能会有多个文件同时包含一个头文件,当这些文件编译链接成一个可执行文件上时,就会出现大量“重定义”错误。在头文件中使用条件编译即可避免该错误。
62. 隐式转换是什么,如何消除类的隐式转换?
(1) C++的基本类型中并非完全的对立,部分数据类型之间是可以进行隐式转换的。所谓隐式转换,是指不需要用户干预,编译器私下进行的类型转换行为。很多时候用户可能都不知道进行了哪些转换。最常见的隐式转换为函数传参。
(2) C++中提供了explicit关键字,在构造函数声明的时候加上explicit关键字,能够禁止隐式转换。
63. 如何在不使用额外空间的情况下,交换两个数?
64. 你知道strcpy和memcpy的区别是什么?
(1) 复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类对象等。
(2) 复制的方法不同。strcpy不需要指定长度,它遇到被复制字符串的结束符”\0”才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度,更加安全。
(3) 用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy。
65. 如果有一个空类,它会默认添加哪些函数?
(1) 默认构造函数
(2) 拷贝构造函数
(3) 析构函数
(4) 赋值运算符函数
66. static_cast比C语言中的转换强在哪里?
(1) 更加安全
(2) 更直接明显,能够一眼看出是什么类型转换为什么类型,容易找出程序中的错误
67. 成员函数里memset(this,0,sizeof(*this))会发生什么?
有时候类里面定义了很多int,char,struct等c语言里的那些类型的变量,我习惯在构造函数中将它们初始化为0,但是一句句的写太麻烦,所以直接就memset(this, 0, sizeof *this);将整个对象的内存全部置为0。
对于这种情形可以很好的工作,但是下面几种情形是不可以这么使用的;
(1) 类含有虚函数表:这么做会破坏虚函数表,后续对虚函数的调用都将出现异常;
(2) 类中含有C++类型的对象:例如,类中定义了一个list的对象,由于在构造函数体的代码执行之前就对list对象完成了初始化,假设list在它的构造函数里分配了内存,那么我们这么一做就破坏了list对象的内存。
68. 你知道回调函数吗?它的作用?
(1) 回调函数就是一个通过函数指针调用的函数。回调的函数的定义由程序员实现,但无需程序员调用,可将函数指针作为参数传递给某个函数库中的函数,由函数库去调用。
(2) 回调函数是在“你想让别人的代码执行你的代码,而别人的代码你又不能动”这种需求下产生的。
(3) 可以做回调函数的函数有两种,一种是普通函数,一种是静态成员函数。普通成员函数不能做回调函数,因为普通成员函数自带this指针参数,会导致函数声明与调用不匹配的情况发生。
(4) 回调函数是一种设计系统的思想,能够解决系统架构中的部分问题,但是系统中不能过多使用回调函数,因为回调函数会改变整个系统的运行轨迹和执行顺序,耗费资源,而且会使得代码晦涩难懂。
69. C++从代码到可执行程序经历了什么?
(1) 预编译
主要处理源代码文件中的以“#”开头的预编译指令。
(2) 编译
把预编译之后生成的xxx.i或xxx.ii文件,进行一系列词法分析、语法分析、语义分析及优化后,生成相应的汇编代码文件。
(3) 汇编
将汇编代码转变成机器可以执行的指令(机器码文件)。 汇编器的汇编过程相对于编译器来说更简单,没有复杂的语法,也没有语义,更不需要做指令优化,只是根据汇编指令和机器指令的对照表一一翻译过来。
(4) 链接
将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。链接分为静态链接和动态链接。
70. 类的对象存储空间?
(1) 非静态成员的数据类型大小之和。
(2) 编译器加入的额外成员变量(如指向虚函数表的指针)。
(3) 为了内存对齐而补入的额外空间。
(4) 空类大小为1,但若是作为基类,则大小为0。
71. 静态链接和动态链接的区别?
静态链接和动态链接是针对函数库来说的,现在的函数库分为两种:静态库和动态库。他们分别对应静态链接和动态链接。
(1) 静态链接:所有的函数和数据都被编译进一个文件中。在使用静态库的情况下,在编译链接可执行文件时,链接器从函数库中复制函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件。静态链接有以下特点:
① 空间浪费:因为每个可执行程序中都拥有函数库数据的一份副本,所以如果多个程序对同一个目标文件都有依赖,当他们同时运行在计算机上,会出现同一个函数库在内存中有多个副本,浪费内存空间;
② 更新困难:每当库函数的代码修改了,所有依赖该库的程序都需要重新进行编译链接。
③ 运行速度快:因为在可执行程序中已经具备了所有执行程序所需要的任何东西,所以在执行的时候运行速度快。
(2) 动态链接:动态链接会在程序运行时才将函数库与源程序执行链接,而不是像静态链接一样把所有模块都链接成一个单独的可执行文件。动态链接有以下特点:
① 共享库:如果多个程序都依赖同一个库,该库不会像静态链接那样在内存中存在多份副本,而是这多个程序在执行时共享同一份副本;
② 更新方便:更新时只需要替换原来的库文件,依赖它的程序无需重新编译。当程序下一次运行时,新库会被自动加载到内存并且与其他程序执行链接,程序就完成了升级迭代。
性能损耗:因为把链接推迟到了程序运行时,每次执行程序都需要进行链接,所以性能会有一定损失。
72. 为什么不能把所有的函数写成内联函数?
(1) 首先,不管是什么函数声明为内联函数,在语法上没有错误。因为inline同register一样,只是个建议,编译器并不一定真正的内联。
(2) 内联函数以代码复杂为代价,省去了函数调用的开销来提高执行效率。如果内联函数体内代码执行时间相比起函数调用开销更大,则没有太大的意义。一般来说若是函数体代码比较长或者内部带有循环,则不推荐使用内联函数。
(3) 将构造函数和析构函数声明为inline是没有什么意义的,即编译器并不真正对声明为inline的构造和析构函数进行内联操作,因为编译器会在构造和析构函数中添加额外的操作(申请/释放内存,构造/析构对象等),致使构造函数/析构函数并不像看上去的那么精简。
(4) 将虚函数声明为inline,要分情况讨论。当指向派生类的指针(多态性)调用声明为inline的虚函数时,由于inline是编译期决定的,而虚函数是运行期决定的,在不知道将要调用哪个函数的情况下,编译器不会内联展开;当对象本身调用虚函数时,编译器能决议出会调用哪个函数时,就会内联展开,当然前提依然是函数并不复杂的情况下。
73. 为什么C++没有垃圾回收机制?
(1) 实现一个垃圾回收器会带来额外的空间和时间开销。
(2) 垃圾回收会使得C++不适合进行很多底层的操作。
74. 说说C++的内存分区?
在C++中,内存分成5个区,他们分别是堆、栈、全局/静态存储区和常量存储区和代码区。
(1) 栈:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈工作效率很高,但是分配的内存容量有限。
(2) 堆:就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。
(3) 全局/静态存储区:这块内存在编译时已经分配好,且在程序运行期间都存在。它主要存放静态数据(局部static变量,全局static变量)、全局变量。
(4) 常量存储区:这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。
(5) 代码区:存放程序的二进制代码。
75. 说说友元函数和友元类的特性?
友元提供了不同类的成员函数之间、类的成员函数和一般函数之间进行数据共享的机制。通过友元,外部的普通函数或者另一个类中的成员函数可以访问本类中的私有成员和保护成员。友元的正确使用能提高程序的运行效率,但同时也破坏了类的封装性和数据的隐藏性,导致程序可维护性变差。友元具有以下特性:
(1) 友元关系不能被继承。
(2) 友元关系是单向的,不具有交换性。若类B是类A的友元,类A不一定是类B的友元,要看在类中是否有相应的声明。
友元关系不具有传递性。若类B是类A的友元,类C是B的友元,类C不一定是类A的友元,同样要看类中是否有相应的声明。
76. 关于this指针你知道什么?
(1) this指针是类的指针,指向对象的首地址。
(2) this指针只能在普通成员函数中使用,在全局函数、静态成员函数中都不能用this。
(3) this在成员函数的开始执行前构造,在成员的执行结束后清除。
(4) this指针只有在普通成员函数中才有定义,且存储位置会因编译器不同有不同存储位置。
(5) this指针主要可用于返回类对象本身的时候,直接使用 return *this。或者当形参数与成员变量名相同时用于区分,如this->n = n。
77. 在成员函数中调用delete this会出现什么问题?
当调用delete this时,类对象的内存空间被释放。因为类成员函数并没有存放在类对象的内存空间中,所以在delete this之后进行的其他任何函数调用,只要不涉及到this指针的内容,都能够正常运行。一旦涉及到this指针,如操作数据成员,调用虚函数等,因为内存被释放,所以就会出现不可预期的问题。
78. 如果在类的析构函数中调用delete this,会发生什么?
会导致堆栈溢出。因为delete会调用析构函数,导致递归。
79. C++中类的数据成员和成员函数内存分布情况?
(1) 普通数据成员存放在对象内存空间中
(2) 静态数据成员存放在全局/静态存储区中
(3) 普通成员函数和静态成员函数都存放在代码区中
80. C++的多态怎么实现?
C++的多态机制有两种,分为编译时多态和运行时多态,下面分别介绍这两种多态。
编译时多态
(1) 主要通过模板和函数重载实现,在编译期发生,由编译器进行推断决议。
(2) 优点:
① 它带来了泛型编程的概念,使得C++拥有泛型编程与STL这样的强大武器。
② 在编译器完成多态,提高运行期效率。
③ 具有很强的适配性与松耦合性,对于特殊类型可由模板偏特化、全特化来处理。
(3) 缺点:
① 程序可读性降低,代码调试带来困难。
② 无法实现模板的分离编译,当工程很大时,编译时间不可小觑。
运行时多态
(1) 运行期多态的设计思想要归结到类继承体系的设计上去。对于有相关功能的对象集合,我们总希望能够抽象出它们共有的功能集合,在基类中将这些功能声明为虚接口(虚函数),然后由子类继承基类去重写这些虚接口,以实现子类特有的具体功能。
(2) 在C++中,主要由虚表和虚表指针实现运行时多态。运行时多态实现细节如下:
① 编译器在发现基类中有虚函数时,会自动为每个含有虚函数的类生成一份虚表,该表是一个一维数组,虚表里保存了虚函数的入口地址。
② 编译器会在每个对象的前四个字节中保存一个虚表指针,即vptr,指向对象所属类的虚表。在构造时,根据对象的类型去初始化虚指针vptr,从而让vptr指向正确的虚表,从而在调用虚函数时,能找到正确的函数
③ 在派生类定义对象时,会先调用父类的构造函数,此时,编译器只“看到了父类,并为父类对象初始化虚表指针,令它指向父类的虚表;当调用子类的构造函数时,为子类对象初始化虚表指针,令它指向子类的虚表
④ 当派生类对基类的虚函数没有重写时,派生类的虚表指针指向的是基类的虚表;当派生类对基类的虚函数重写时,派生类的虚表指针指向的是自身的虚表;当派生类中有自己的虚函数时,在自己的虚表中将此虚函数地址添加在后面
(3) 优点:
面向对象设计中重要的特性,对客观世界直觉认识。
(4) 缺点:
① 运行期间进行虚函数绑定,提高了程序运行开销。
② 庞大的类继承层次,对接口的修改易影响类继承层次。
③ 由于虚函数在运行期在确定,所以编译器无法对虚函数进行优化。
④ 虚表指针增大了对象体积,类也多了一张虚函数表。
81. 为什么要把析构函数写成虚函数?
由于类的多态性,基类指针可以指向派生类的对象。如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全,造成内存泄漏。
82. 虚函数表存放在内存的什么区域?
C++中虚函数表位于只读数据段(.rodata),也就是C++内存模型中的常量区;而虚函数则位于代码段(.text),也就是C++内存模型中的代码区。
83. 模板偏特化了解吗?
(1) 通过编写模板,能适应多种类型的需求,使每种类型都具有相同的功能,但对于某种特定类型,如果要实现其特有的功能,单一模板就无法做到,这时就需要模板特例化(模板偏特化)。
(2) 所谓的模板偏特化对单一模板提供的一个特殊实例,它将一个或多个模板参数绑定到特定的类型或值上。
(3) 特例化的本质是实例化一个模板,而非重载它。特例化不影响参数匹配。参数匹配都以最佳匹配为原则。
(4) 可以特例化类中的部分成员函数而不是整个类。
84. 哪些函数不能被定义为虚函数?
(1) 构造函数。每一个声明了虚函数的类对象都有一个指向虚表(vtable)的指针,但是这个指向vtable的指针事实上是存储在对象的内存空间的,假设构造函数是虚的,就须要通过 vtable来调用,但是对象还没有实例化,也就是内存空间还没有,怎么找vtable呢?再者,虚函数主要是在调用对象不确定的情况下使用的,然而构造函数本身就是要初始化实例,那使用虚函数也没有实际意义。
(2) 静态函数。静态函数不属于对象属于类,静态成员函数没有this指针,因此静态函数设置为虚函数没有任何意义。
(3) 友元函数。友元函数不属于类的成员函数,不能被继承。对于没有继承特性的函数没有虚函数的说法。
(4) 普通函数。普通函数不属于类的成员函数,不具有继承特性,因此普通函数没有虚函数。
85. 构造函数和析构函数可以调用虚函数吗,为什么?
(1) 构造函数调用虚函数没有意义,因为父类对象会在子类之前进行构造,此时子类部分的数据成员还未初始化,因此调用子类的虚函数时不安全的,故而C++不会进行动态联编。
(2) 析构函数调用虚函数没有意义,析构函数是用来销毁一个对象的,在销毁一个对象时,先调用子类的析构函数,然后再调用基类的析构函数。所以在调用基类的析构函数时,派生类对象的数据成员已经销毁,这个时候再调用子类的虚函数没有任何意义。
86. 构造函数和析构函数可否抛出异常?
(1) 构造函数不可抛出异常:C++只会析构已经完成的对象,对象只有在其构造函数执行完毕才算是完全构造妥当。在构造函数中发生异常,控制权转出构造函数之外。因此,如果某个对象的构造函数中发生异常,则该对象的析构函数不会被调用。因此会造成内存泄漏。
(2) 析构函数不可抛出异常:如果析构函数抛出异常,则异常点之后的程序不会执行,如果析构函数在异常点之后执行了某些必要的动作比如释放某些资源,则这些动作不会执行,会造成诸如资源泄漏的问题。再者,通常异常发生时,c++的机制会调用已经构造对象的析构函数来释放资源,此时若析构函数本身也抛出异常,则前一个异常尚未处理,又有新的异常,会造成程序崩溃的问题。
87. 模板类和模板函数的区别是什么?
函数模板的实例化是由编译器在处理函数调用时自动完成的,而类模板的实例化必须由程序员在代码中显式地指定。即函数模板允许隐式调用和显式调用,而类模板只能显式调用。
88. 什么是虚继承?
(1) 由于C++支持多继承,因此除了public、protected和private三种继承方式外,还支持虚拟(virtual)继承。
(2) 多继承有可能引发一直特别情况:B和C公有继承A,D又公有继承B和C,这种方式是一种菱形继承或者钻石继承,如下图所示。
(3) 如果D调用了A的方法,则会引发数据的二义性和冗余,编译器不知道是要调用B所继承的A,还是C所继承的A。
(4) 为了解决这个问题,C++引入了虚拟继承,在虚拟继承的情况下,无论基类被继承多少次,只会存在一个实体。
89. 抽象基类为什么不能创建对象?纯虚函数又是什么?
(1) 带有纯虚函数的类为抽象类。抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的。抽象类会将所有派生类中有关的操作抽象成一个通用接口且不做具体实现(纯虚函数),具体实现由派生类实现,因此抽象基类不可以创建对象。
(2) 纯虚函数是一种特殊的虚函数,该类函数没有函数体。因为在许多情况下,在基类中不能对虚函数给出有意义的实现,而把它声明为纯虚函数,它的实现留给该基类的派生类去做。
90. 说说RTTI?
运行时类型识别(Run-time type identification , RTTI),是指在只有一个指向基类的指针或引用时,确定所指对象的准确类型的操作。其常被说成是C++的四大扩展之一(其他三个为异常、模板和名字空间)。使用RTTI有两种方法:
1、typeid()
第一种就像sizeof(),它看上像一个函数,但实际上它是由编译器实现的。typeid()带有一个参数,它可以是一个对象引用或指针,返回全局typeinfo类的常量对象的一个引用。可以用运算符“= =”和“!=”来互相比较这些对象,也可以用name()来获得类型的名称。同时,我们也可以用typeid 检查基本类型和非多态类型。如果想知道一个指针所指对象的精确类型,我们必须逆向引用这个指针。比如:
2、dynamic_cast
该运算符为强制类型转换符,上文第49条有提及
91. 多继承的优缺点,作为一个开发者怎么看待多继承?
多重继承的优点很明显,就是一个对象可以调用多个基类中的接口。多继承容易导致菱形继承问题,虽然可以用虚继承解决该问题,但也会造成内存结构复杂,效率降低,继承体系过于复杂化的缺点。
92. 为什么拷贝构造函数必须传引用不能传值?
拷贝构造函数的作用就是用来拷贝对象的,使用一个已存在的对象来初始化一个新的对象。如果使用传值方式,那么在拷贝构造函数被调用时,在进行参数传递的时候就会调用拷贝构造函数,这样会导致递归溢出。
93. 为什么在C/C++中要将代码分为头文件和源文件,不能写到一起吗?
(1) 将所有代码写入一个文件中当然可以,同样可以通过编译且正常运行。
(2) 分开写的目的是方便未来。有时候我们写的代码会给别人去用,如果是非开源代码,可以将源文件封装起来并生成库文件(库文件是二进制文件,无法查阅代码)。只对外开放头文件和库文件,那么别人就无法看到代码的具体实现了。
C++11
1. auto、decltype的用法?
(1) auto:C++11引入了auto类型说明符,它可以让编译器通过初始值来进行类型推演,使得程序员无需知道类型名称就可以定义变量,所以auto 定义的变量必须有初始值。
(2) decltype: auto定义的变量必须初始化,如果我们不想要初始化就可以使用decltype,它会返回参数的数据类型,并定义新的变量,而且新变量的值不会被初始化。
2. C++中NULL和nullptr的区别?
(1) NULL是一个宏定义,C中NULL为(void*)0,C++中NULL为整数0。
(2) 将NULL定义为0带来的一个问题是无法与整数的0区分,因为C++中允许有函数重载,若是有个a、b两个重载函数,参数分别为整数和指针,那么在传入NULL参数时,会把NULL当做整数0来看,导致错误调用了参数为整数的函数。
(3) nullptr可以解决这一问题,nullptr可以明确区分整型和指针类型,能够根据环境自动转换成相应的指针类型,但不会被转换为任何整型,所以不会造成参数传递错误。
3. 说说final和override关键字?
(1) Override指定了子类的这个虚函数是对父类虚函数的重写,如果函数名不小心打错了的话,编译器会进行报错。
(2) 当不希望某个类被继承,或不希望某个虚函数被重写,可以在类名和虚函数后添加final关键字,添加final关键字后如果被继承或重写,编译器会报错。
4. C++中的智能指针?
(1) 智能指针会管理程序员申请的内存,在使用结束后会自动释放,防止堆内存泄漏。
(2) auto_ptr:最原始的智能指针。auto_ptr采用的是独享所有权语义,一个非空的auto_ptr总是拥有它所指向的资源,转移一个auto_ptr将会把所有权全部从源指针转移给目标指针,源指针被置空。由于支持拷贝语义,拷贝后源对象变得无效,如果程序员忽视了这点,这可能引发很严重的问题。在C++11中该指针已被弃用。
(3) unique_ptr:与auto_ptr类似,采用独享所有权语义。unique_ptr提供移动语义,这在很大程度上避免了auto_ptr的错误,因为很明显必须使用std::move()进行转移,提醒程序员在这个地方发生了移动。
(4) shared_ptr:采用引用计数器的方法,允许多个智能指针指向同一个对象,每当多一个指针指向该对象时,指向该对象的所有智能指针内部的引用计数加1,每当减少一个智能指针指向对象时,引用计数会减1,当计数为0的时候会自动的释放动态分配的资源。引用计数器的变化依据如下所示。
① 每次创建类的新对象时,初始化指针并将引用计数置为1
② 当对象作为另一对象的副本而创建时,拷贝构造函数会拷贝指针并增加与之相应的引用计数
③ 对一个对象进行赋值时,赋值操作符减少左操作数所指对象的引用计数(如果引用计数为减至0,则删除对象),并增加右操作数所指对象的引用计数
④ 调用析构函数时,构造函数减少引用计数(如果引用计数减至0,则删除基础对象)
(5) weak_ptr: 由于shared_ptr引用计数存在的问题,即互相引用形成环(环形引用),使得两个指针指向的内存都无法释放,如下所示。
① 为了解决这个问题,C++引入了weak_ptr(弱引用),它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,也就是说,它只引用,不计数。
② 如果一块内存被shared_ptr和weak_ptr同时引用,当所有shared_ptr析构了之后,不管还有没有weak_ptr引用该内存,内存也会被释放。
③ weak_ptr不保证它指向的内存一定是有效的,在使用之前应先检查weak_ptr是否为空指针,避免访问非法内存,也因此weak_ptr并不能直接访问对象,他只能通过转化为shared_ptr来使用对象。
5. 说说STL容器中的智能指针?
具备独占所有权语义的智能指针不能在STL的容器中使用,如auto_ptr和unique_ptr,因为STL容器中的元素经常要支持拷贝、赋值操作,在这过程中auto_ptr会传递所有权,容易导致错误,而unique_ptr又不支持普通的拷贝和赋值操作,也不能用在STL标准容器中。
6. 说说lambda函数?
利用lambda表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象。每当你定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类当然重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,该实例是一个右值。lambda的优点有以下几点:
(1) 距离:很多人认为,让定义位于使用的地方附近很有用。这样,就无需翻阅很多页的源代码,以了解函数。另外,如果需要修改代码,设计的内容就在附近,就很好修改。
(2) 简洁:函数符代码要比lambda代码更加繁琐,函数和lambda的简洁程度相当。
(3) 功能:lambda可以访问作用域内的任何动态变量,可以采用取值、引用的形式进行捕获。
7. 什么是声明时初始化?
C++11新增了类成员初始化新机制——声明时初始化,可以直接在类中声明数据成员时就进行初始化操作,而不用借助构造函数或者初始化列表。
8. C++11添加哪几种构造函数关键字?
(1) default关键字可以显式要求编译器生成默认构造函数,防止在调用时相关构造函数没有定义而报错。
(2) delete关键字可以删除构造函数、赋值运算符函数等,在使用时编译器会报错
9. 说说C++的左值和右值?
(1) 在C++11中所有的值必属于左值、右值两者之一,右值又可以细分为纯右值、将亡值。
(2) 在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。
(3) 纯右值的概念等同于我们在C++98标准中右值的概念,指的是临时变量和字面量值;将亡值则是C++11新增的新右值,它表示该对象的内存空间接下来会被其他对象接管。通过这种接管空间的方式可以避免内存空间的释放和分配,延长变量值的生命期。
(4) C++11对于引用了也有了新的解释。传统的C++引用被称为左值引用,符号为&,他关联的是左值。C++11中增加了右值引用,符号为&&。右值引用会关联到右值,右值被存储到特定位置,右值引用会指向该特定位置,也就是说,右值虽然无法获取地址,但是右值引用是可以获取地址的,该地址表示临时对象的存储位置。使用std::move可以使一个左值转换为右值引用。
10. 说说移动构造函数?
(1) 移动构造是C++11标准中提供的一种新的构造方法,用来给予程序员新的构造选择,用以替换拷贝构造。
(2) 拷贝构造函数是先将传入的参数对象进行一次深拷贝,再传给新对象。这就会有一次拷贝对象的开销,拷贝的内存越大越耗费时间,并且进行了深拷贝,就需要给对象分配地址空间。
(3) 移动构造函数会直接接管源对象空间,既不会产生额外的拷贝开销,也不会给新对象分配内存空间。提高程序的执行效率,节省内存消耗。
(4) 移动构造函数的参数必须是自身类型的右值引用,也就是说能调用移动构造函数的参数必然是个右值(纯右值和将亡值)。
11. 什么是列表初始化?
列表初始化是C++ 11新引进的初始化方式,它采用一对花括号(即{})进行初始化操作。能用直接初始化和拷贝初始化的地方都能用列表初始化,而且列表初始化能对容器进行方便的初始化,因此在新的C++标准中,推荐使用列表初始化的方式进行初始化。
12. 初始化列表和列表初始化的区别?
(1) 初始化列表是在创建类对象时,对类对象内部的数据成员进行的一种初始化方式,具体用在类的构造函数中。
(2) 列表初始化是C++11引入的一种新的对象初始化方式,它采用一对花括号(即{})进行初始化操作。主要用在类对象定义时,为它指定初始值。
STL
1. 什么是STL?
Standard Template Library(标准模板库),是C++的标准库之一,它是一套基于模板的容器类库,还包括许多常用的算法,提高了程序开发效率和复用性。STL包含6大部件:容器、迭代器、算法、仿函数、适配器和空间配置器。
2. SGI的二级空间配置器了解吗?
(1) 对象构造前的空间配置和对象析构后的空间释放,由<stl_alloc.h>负责,SGI设计了双层级配置器:
① 第一级空间配置器直接使用malloc和free,如果在申请动态内存时找不到足够大的内存块,将返回NULL 指针,宣告内存申请失败。
② 第二级空间配置器视情况使用不同的策略,当申请内存大于128字节时,调用第一级配置器。当申请内存小于128b字节时,采用内存池方式,维护16个(128/8)自由链表,每个链表维护8字节大小的内存块,从中进行内存分配,如果内存不足转第一级配置器处理。
(2) 二级空间配置器存在的问题:
① 自由链表所挂区块都是8的整数倍,因此当我们需要非8倍数的区块,往往会导致浪费。
② 由于配置器的链表都是静态变量,他们存放在全局/静态区,其释放时机就是程序结束,这样子会导致自由链表一直占用内存。
3. traits技法?
(1) 在 STL 编程中,容器和算法是独立设计的,连接容器和算法的桥梁就是迭代器。在算法中我们可能会定义简单的中间变量或者设定算法的返回变量类型,这时候需要知道迭代器所指元素的类型是什么,这正是traits技法的用途。
(2) 首先,在算法中运用迭代器时,假设算法中有必要声明一个变量,以”迭代器所指对象的型别”为型别,该怎么办呢?我们可以使用function template 的参数推导机制,他可以由某个对象的指针推导出某个对象的类型。
(3) 但是,函数的”template 参数推导机制”推导的只是参数,无法推导函数的返回值类型。万一需要推导函数的传回值,就无能为力了。以下代码会编译失败。
(4) 要想推导函数的返回值类型,声明内嵌型别是个不错的主意。只要做一个iterator,对真正的指针进行封装,然后在定义的时候为其指向的对象类型制定一个别名,如value_type。
(5) 这个方法也有缺陷,对于func来说,它属于泛型编程,那么他就应该可以结束任意类型的迭代器,包括原生指针(int、double、char),但显然func不能接受原生指针,因为原生指针根本就没有 value_type 这个内嵌类型。幸运的是,C++模板提供了偏特化操作,他可以让我们对原生指针做特殊处理,如下所示。
(6) 这似乎完美了,我们可以借由内嵌型别和模板偏特化来获取某个迭代器指向对象的类型,并可以将它声明为参数或者是函数返回值。但这个方法还有改进的空间,因为这种偏特化是针对可调用函数 func 的偏特化,假如 func 有 100 万行代码,那么这就意味着我们需要为它实现多个偏特化版本,这会造成极大的代码重复。因此traits技法开始发挥作用了。
(7) 简单来说,traits技法会为迭代器和算法之间加入一个中间层,由它替代迭代器去回答它所指向的对象的类型。如果是原生指针类型,则直接返回指针指向的类型。如果是自定义的迭代器类型,则转去询问迭代器(T::value_type),并代替它返回迭代器指向的类型。
(8) 简单来说,通过 traits 技法(背后是内嵌型别与模板偏特化的组合),我们将函数模板对于原生指针和自定义 iterator 的定义都统一起来,我们使用 traits 技法主要是为了解决原生指针和自定义 iterator 之间的不同所造成的代码冗余,这就是 traits 技法的妙处所在。
4. 说说STL中的容器?
(1) vector
① vector底层是一个动态数组,包含三个迭代器:start、finish、end_of_storage。start和finish之间是已经被使用的空间范围,表示当前vector中有多少个元素,即有效空间size。start和end_of_storage是整块连续空间包括备用空间的大小,表示它分配的内存中可以容纳多少元素,即容量capacity。
② 当空间不够装下数据(vec.push_back(val))时,会自动申请另一片更大的空间(1.5倍或者2倍),然后把原来的数据拷贝到新的内存空间,接着释放原来的那片空间。之所以是1.5倍或者2倍,是因为考虑到可能产生的堆空间浪费,增长倍数不能太大,使用1.5或者2是比较合理的倍数。
③ 当释放或者删除(vec.clear())里面的数据时,其存储空间不释放,仅仅是清空了里面的数据。
④ 对vector的任何操作一旦引起了空间的重新配置,指向原vector的所有迭代器都会失效。
⑤ reserve函数的作用:将vector直接扩充到确定的大小,可以减少多次开辟和释放空间的效率问题(优化push_back),还可以减少拷贝数据的次数,它直接更改capacity。
⑥ resize()函数的作用:可以改变vector有效空间的大小,即size的大小。如果size大于capacity,capacity的大小也会随着改变。
⑦ vector的底层实现要求连续的对象排列,引用并非对象,没有实际地址,因此vector的元素类型不能是引用。
⑧ 当删除容器中一个元素后,该迭代器所指向的元素已经被删除,那么也造成迭代器失效。erase方法会返回下一个有效的迭代器。
⑨ 释放vector内存的方法:
vec.clear():清空内容,但是不释放内存。
vector
vec.shrink_to_fit():请求容器降低其capacity和size匹配。
vec.clear();vec.shrink_to_fit();:清空内容,且释放内存。
(2) List
① list的底层是一个双向链表,以结点为单位存放数据,结点的地址在内存中不一定连续,每次插入或删除一个元素,就配置或释放一个元素空间。
② list不支持随机存取,如果需要大量的插入和删除,而不关心随机存取,则可以使用list。
(3) Deque
① deque的是一个双向开口的连续线性空间(双端队列),在头尾两端进行元素的插入跟删除操作都有理想的时间复杂度。
② deque的底层并不是真正连续的空间,而是由一段段连续的小空间拼接而成,实际deque类似于一个动态的二维数组,由一个map(中控指针数组)和多个连续的缓冲区组成:
③ 当deque不断增加元素时,一旦map(中控指针数组)满了,那么会增容,不过map增容的代价非常低,因为只需要拷贝存储数据的buffer数组的指针,不需要拷贝buffer中的内容。
④ 双端队列底层是一段假象的连续空间,实际是分段连续的,为了维护其“整体连续”以及随机访问的假象,deque的迭代器设计就比较复杂。由cur、first、last指向当前遍历buffer数组,node指向map中的元素,遍历deque的操作由这几个指针进行维护。
⑤ deque并不是从map的第一个位置就开始存放元素,而是从中间开始存放,这样在头部和尾部插入元素就会变得容易。
⑥ 与vector比较:头部插入和删除时,不需要搬移元素,效率特别高,而且在扩容时,也不需要搬移大量的元素,因此其效率是必vector高的。
⑦ 与list比较:其底层是连续空间,空间利用率比较高,不需要存储额外字段。
⑧ 不适合遍历,因为在遍历时,deque的迭代器要频繁的去检测其是否移动到某段小空间的边界,导致效率低下。不适合大量的中间插入删除,也不适合大量的随机访问。
(4) map 、set、multiset、multimap
① 底层数据结构都是红黑树,是一种自平衡的二叉搜索树
② set和multiset会根据特定的排序准则自动将元素排序,set中元素不允许重复,multiset可以重复。
③ map和multimap将key和value组成的键值对作为元素,根据key的排序准则自动将元素排序,map中元素的key不允许重复,multimap可以重复。
④ map和set的增删改查速度为都是logn,是比较高效的。
⑤ map和set的插入删除效率比序列容器高,而且每次insert之后,以前保存的iterator不会失效。因为存储的是结点,不需要内存拷贝和内存移动。
(5) unordered_map、unordered_set
① 底层数据结构是一个防冗余的哈希表(采用除留余数法)。其数据的存储和查找的效率很高,时间复杂度为O(1);而代价仅仅是消耗比较多的内存。
② 使用开链法解决哈希冲突。
③ 数据的存放是无序的
5. 什么是STL的顺序容器和关联式容器?
(1) 关联容器(Associative Container)与顺序容器(Sequential Container)的本质区别在于:关联容器是通过键(key)存储和读取元素的,而顺序容器则通过元素在容器中的位置顺序存储和访问元素。
(2) 在STL中,这里的“顺序”和“关联”指的是上层接口表现出来的访问方式,并非底层存储方式。为什么这样划分呢?因为对STL的用户来说,他们并不需要知道容器的底层实现机制,只要知道如何通过上层接口访问容器元素就可以了,否则违背了泛型容器设计的初衷。
(3) 顺序容器主要采用向量和链表及其组合作为基本存储结构,如堆栈和各种队列。而关联式容器采用平衡二叉搜索树作为底层存储结构。
6. 说说STL的容器适配器?
容器适配器,其就是将不适用的序列式容器(包括 vector、deque 和 list)变得适用。容器适配器的底层实现都是通过封装某个序列式容器,并重新组合该容器中包含的成员函数,使其满足某些特定场景的需要。
(1) stack
① stack(栈)是一种先进后出(First In Last Out)的数据结构,只有一个出入口,那就是栈顶,除了对栈顶元素进行操作外,没有其他方法可以操作内部的其他元素。
② C++的栈是一种容器适配器,其底层数据结构一般用list或deque实现,只开放一部分的接口和方法即可完成对栈的支持。
(2) queue
① queue(队列)是一种先进先出(First In First Out)的数据结构,只有一个入口和一个出口,分别位于队头与队尾,只能在队尾插入元素,在队头取出元素,没有其他方法可以操作内部的其他元素。
② C++的队列是一种容器适配器,其底层数据结构一般用list或deque实现,只开放一部分的接口和方法即可完成对队列的支持。
(3) priority_queue
① priority_queue,优先级队列,是一个拥有权值观念的queue,它跟queue一样只能在队尾插入元素,在队头取出元素。在插入元素时,元素并非按照插入次序排列,它会自动根据权值(通常是元素的实值)排列,权值最高,排在最前面。
② priority queue(优先队列)的底层实现机制实际上是堆,因为大根堆总是最大值位于堆的根部,优先级最高。
③ C++中的堆是容器适配器,一般是vector为底层容器,以堆的处理规则来进行管理。
7. 说说STL的迭代器?
(1) 迭代器是连接容器和算法的一种重要桥梁,通过迭代器可以在不了解容器内部原理的情况下遍历容器。
(2) 在遍历容器的时候,不可避免的要对遍历的容器内部有所了解,所以,干脆把迭代器的开发工作交给容器的设计者好了,如此以来,所有实现细节反而得以封装起来不被使用者看到,这正是为什么每一种 STL 容器都提供有专属迭代器的缘故。
(3) 迭代器种类分为5类:
① 输入迭代器:是只读迭代器,在每个被遍历的位置上只能读取一次。
② 输出迭代器:是只写迭代器,在每个被遍历的位置上只能被写一次。
③ 前向迭代器:兼具输入和输出迭代器的能力,但是它可以对同一个位置重复进行读和写。但它不支持operator–,所以只能向前移动。
④ 双向迭代器:很像前向迭代器,只是它向后移动和向前移动同样容易。
⑤ 随机访问迭代器:有双向迭代器的所有功能。而且,它还提供了“迭代器算术”,即在一步内可以向前或向后跳跃任意位置, 包含指针的所有操作,可进行随机访问,随意移动指定的步数。
(4) STL每种容器对应的迭代器
(5) 通过traits技法,我们可以获取到迭代器一些特性,STL规定,每一个迭代器至少包含以下几种特性供外界获取,方便算法使用迭代器。
① value_type:迭代器所指对象的类型
② difference_type:两个迭代器之间的距离
③ pointer:迭代器所指对象的指针类型
④ reference:迭代器所指对象的引用类型
⑤ iterator_category:迭代器种类
(6) 迭代器失效问题
① 数组型数据结构(vector):该数据结构的元素是分配在连续的内存中,insert和erase操作,都会使得删除点和插入点之后的元素挪位置,所以,插入点和删除掉之后的迭代器全部失效,也就是说insert( * iter)(或erase( * iter)),然后再iter++,是没有意义的。解决方法:erase( * iter)的返回值是下一个有效迭代器的值。 iter =cont.erase(iter)。
② 链表型数据结构:对于list型的数据结构,使用了不连续分配的内存,删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器.解决办法两种,erase( * iter)会返回下一个有效迭代器的值,或者erase(iter++)。
③ 树形数据结构: 使用红黑树来存储数据,插入不会使得任何迭代器失效;删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器。erase迭代器只是被删元素的迭代器失效,但是返回值为void,所以要采用erase(iter++)的方式删除迭代器。
④ Deque:插入头尾会使迭代器全部失效但是引用不失效。其原因在于插入头尾可能会进行扩容,由于map的重新分配,迭代器的node失效,但是原map指向的连续数组并没有重新分配。因此,对整个迭代器来说是失效的,但对于元素的指针和引用仍然是有效的。删除头尾会使被删除的元素迭代器和引用失效,插入和删除中间会使迭代器和引用全部失效。
⑤ unodered_map/unordered_set:由于底层是哈希表,迭代器是否失效主要看哈希表的实现策略,对于使用除留余数法和开链法的哈希表来说,删除运算使指向删除位置的迭代器失效,但是不会失效其他迭代器。
8. 说说STL容器的线程安全性?
STL只保证最低限度的线程安全性,即:多个读者是安全的,多线程可以同时读取一个容器的内容,但如果有多个写者,则必须使用同步互斥机制保证线程安全。
9. STL容器的使用场景?
(1) vector的使用场景:只查看,而不频繁插入删除的
(2) deque的使用场景:头尾需要频繁插入删除
(3) list的使用场景:频繁的插入删除的场景,且位置不固定
(4) Set:针对单一值的增删查改操作都要有,且要求数据有序
(5) Map:针对键值对的增删查改操作都要有,且要求数据有序
(6) unordered_set:针对单一值的增删查改操作都要有,数据排列无要求。
(7) unordered_map:针对键值对的增删查改操作都要有,数据排列无要求。
10. 什么是trivial destructor?
(1) “trivial destructor”一般是指用户没有自定义析构函数,而由系统生成,这种析构函数在《STL源码解析》中称为“无关痛痒”的析构函数,对于trivial destructor,如果每次对象析构时都进行调用,会造成效率下降。
(2) 如果用户自定义了析构函数,则称之为“non-trivial destructor”,这种析构函数在对象析构时必须被调用,否则可能会造成内存泄露。
(3) STL提供了traits技法来判断某个类的析构函数是否为trivial destructor,再决定是否调用析构函数。
(4) STL实际上对于类的特性判断共有4种:non-trivial defalt ctor(默认构造函数)、non-trivial copy ctor(拷贝构造函数)、non-trivial assignment operator(赋值运算符) 、non-trivial dtor(析构函数),通过这些特性来判断是否可以采取直接操作内存的方式提高效率。
更新记录
更新日期 | 更新详情 |
---|---|
(2022年05月11日) | 内容汇总,主要吸收了拓跋阿秀校招笔记的内容,以及部分面经 |
(2023年04月22日) | 排版升级,分区域总结,如C++98、C++11、STL,更容易查找相关内容。进行笔记整理,不再以面经为导向,而是专注于笔记精华部分,删减了大量重复内容,重新编写一些不清晰的表述,新增了几个笔记。 |
(2023年08月31日) | 将各个大板块重新编号,现在各个版块都有独立的题目编号,互不干扰。将更新记录中的版本号替换为年份日期,取消版本号机制,方便记录更新。C++板块增加第 93 问。 |
(2023年09月17日) | 修改首页文字布局,统一化布局。修改前言。添加前提基础模块。更改正文和标题字体。修改部分问题描述。更新目录。更改笔记名字为C++(基础)。修改参考资料。所有的更新日期都添加前置0,统一长度。 |
参考资料
《C++之基础语法》:https://interviewguide.cn/notes/03-hunting_job/02-interview/01-01-01-basic.html
《C++编译期多态与运行期多态》:https://www.cnblogs.com/QG-whz/p/5132745.html
《顺序容器和关联容器的比较》:https://blog.csdn.net/JIEJINQUANIL/article/details/51175858
《C++之lambda函数》:https://blog.csdn.net/sinat_35678407/article/details/82794514
《C++自增运算符进行重载》:https://blog.csdn.net/jiang1013nan/article/details/6106092
《C++运算符重载详解》:https://blog.csdn.net/lishuzhai/article/details/50781753
《STL详解及常见面试题》:https://blog.csdn.net/daaikuaichuan/article/details/80717222
《C++回调函数详解》:https://blog.csdn.net/qq_45311905/article/details/116504377
《C++面试 select poll epoll之间的区别》:https://blog.csdn.net/u014430031/article/details/115430161
《C++面试题–内存相关》:https://www.wangjiaqingll.com/285.html
《【基础知识】c++的变量初始化》:https://blog.csdn.net/All_In_gzx_cc/article/details/125070039
《C++ 移动构造函数详解》:https://blog.csdn.net/weixin_44788542/article/details/126284429
《C++智能指针weak_ptr详解》:https://blog.csdn.net/sinat_31608641/article/details/107702175
《C++ 内存对齐》:https://blog.csdn.net/weixin_43816121/article/details/127754930
《C++ STL 源码剖析之 Traits 编程技法》:https://zhuanlan.zhihu.com/p/85809752/
《【C++】– STL容器适配器之底层deque浅析》: https://blog.csdn.net/gx714433461/article/details/125311186
《迭代器失效的几种情况》: https://blog.csdn.net/qq_44918090/article/details/120504619