C++八股

1、简述一下C++中的多态

在面向对象中,多态是指通过基类的指针或引用,在运行时动态调用实际绑定对象函数的行为,与之相对应的编译时绑定函数称为静态绑定。
静态多态
静态多态是编译器在编译期间完成的,编译器会根据实参类型来选择调用合适的函数,如果有合适的函数就调用,没有的话就会发出警告或者报错。静态多态有函数重载、运算符重载、泛型编程等。
动态多态
动态多态是在程序运行时根据基类的引用(指针)指向的对象来确定自己具体该调用哪一个类的虚函数。当父类指针(引用)指向父类对象时,就调用父类中定义的虚函数;当父类指针(引用)指向子类对象时,就调用子类中定义的虚函数。

  • 动态多态行为的表现效果

同样的调用语句在实际运行时有多种不同的表现形态

  • 实现动态多态的条件

要有继承关系;要有虚函数重写(被virtual声明的函数叫虚函数);要有父类指针(父类引用)指向子类对象

  • 动态多态的实现原理

在类中声明虚函数时,编译器会在类中生成一个虚函数表,虚函数表是一个存储类虚函数指针的数据结构,虚函数表是由编译器自动生成与维护的。virtual成员函数会被编译器放入虚函数表中,存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr指针)。在多态调用时,vptr指针就会根据这个对象在对应类的虚函数表中查找该调用函数,从而找到函数的入口地址。

2、简述一下什么是面向对象

面向过程的思想
完成一个需求的步骤。面向过程编程,就是面向具体的每一个步骤和过程,把每一个步骤和过程完成,然后由这些功能函数相互调用,完成需求。
面向对象思想
尽可能模拟人类的思维方式,使得软件的开发方式与过程尽可能接近人类认识世界、解决现实问题的方法和过程,把客观世界中的实体抽象为问题域中的对象。面向对象以对象为核心,该思想认为程序由一系列对象组成。

  • 面向对象的三大特征

封装、继承、多态
封装:将事物属性和行为封装在一起,类,便于管理,提高代码的复用性。事物的属性和行为分别对应类中的成员变量和成员方法。
继承:继承使类与类之间产生关系,提高代码的复用性以及维护性。
多态:调用成员函数时,会根据调用方法的对象的类型来执行不同的函数。
面向过程和面向对象解决问题举例-以洗衣服为例

  • 面向过程
    接水到盆中-放入衣服-加入洗衣粉-浸泡-搓衣服-过水-拧干-晾干
  • 面向对象
    将衣服放入洗衣机-加入洗衣粉-开启-晾干

面向对象的方式解决问题更加简单一些,但面向对象还是基于面向过程的。

3、简述一下面向对象的三大特征

封装、继承、多态

  • 封装

将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现,仅对外公开接口来和对象进行交互。
C++通过private、protected、public三个关键字来控制成员变量和成员函数的访问权限,分别表示公有的、受保护的、私有的,成员访问限定符。
private修饰的成员只能在本类中访问;protected表示受保护的权限,修饰的成员只能在本类或者子类中访问;public修饰的成员是公共的,哪儿都可访问。
封装的好处:隐藏实现细节,提供公共的访问方式;提高代码的复用性;提高安全性。

  • 继承

代码复用,通过继承机制可以利用已有的数据类型来定义新的数据类型,新的类不仅拥有旧类的成员,还拥有新定义的成员。
一个B类继承于A类,或称从类A派生类B。类A成员基类(父类),类B成为派生类(子类)。
派生类中的成员,包含两大部分:一类是从基类继承过来的,一类是自己增加的成员。从基类继承过来的表现其共性,而新增的成员体现其个性。
继承的好处:提高代码的复用性;提高代码的拓展性;是多态的前提。

  • 多态

在面向对象中,多态是指通过基类的指针或者引用,在运行时动态调用实际绑定对象函数的行为。多态是指在程序运行时根据基类的引用(指针)指向的对象来确定自己具体该调用哪一个类的虚函数。
当父类指针(引用)指向父类对象时,就调用父类中定义的虚函数;当父类指针(引用)指向子类对象时,就调用子类中定义的虚函数。多态性改善了代码的可读性和组织性,同时也使创建的程序具有可扩展性。

4、简述一下浅拷贝和深拷贝

浅拷贝和深拷贝最根本的区别在于是否真正获取一个对象的复制实体,而不是“引用”,一般在拷贝构造函数和赋值运算符重载函数中涉及到。
浅拷贝:
值拷贝,将源对象的值拷贝到目标对象中,如果对象中有某个成员是指针类型数据,并且是在堆区创建,则使用浅拷贝仅拷贝这个指针变量的值,在目标对象中该指针类型数据和源对象中的该成员指向同一块堆空间。带来问题:在析构函数中释放该堆区数据,会被释放多次。默认的拷贝构造函数和默认的赋值运算符重载函数都是浅拷贝。
深拷贝:
深拷贝在拷贝的时候先开辟出和源对象大小一样的空间,然后将源对象里的内容拷贝到目标对象中去,指针成员就指向了不同的内存位置,并且里面的内容是一样的,不仅达到拷贝目的,还不会出现问题,两个对象先后去调用析构函数,分别释放自己指针成员所指向的内存。
每次增加一个指针,便申请一块新的内存,并让这个指针指向新的内存,深拷贝情况下,不会出现重复释放同一块内存的错误。

5、请你说说三种智能指针实现原理和使用场景,以及其线程安全

1. 智能指针实现原理
建立所有权(ownership)概念,对于特定的对象,只有一个智能指针可拥有它,这样只有拥有对象的智能指针的析构函数会删除该对象。然后,让赋值操作转让所有权。这就是用于auto_ptr和unique_ptr的策略,但unique_ptr的策略更严格,unique_ptr能够在编译期识别错误。
跟踪引用特定对象的智能指针计数,称为引用计数。如:赋值时,计数将加1,而指针过期时,计数将减1,仅当最后一个指针过期时,才调用delete。这是shared_ptr采用的策略。

2.使用场景
如果程序要使用多个指向同一个对象的指针,应该选择shared_ptr;如果程序不需要多个指向同一个对象的指针,则可以使用unique_ptr;如果使用new[]分配内存,应选择unique_ptr;如果函数使用new分配内存,并返回指向该内存的指针,将其返回类型声明为unique_ptr是不错的选择。

3.线程安全
shared_ptr智能指针的引用计数在手段上使用了atomic原子操作,只要shared_ptr在拷贝或赋值时增加引用,析构时减少引用就可以了。首先原子是线程安全的,所以shared_ptr智能指针在多线程下引用计数也是线程安全的。shared_ptr智能指针在多线程下传递使用时引用计数是不会有线程安全的。但是指向对象的指针不是线程安全的,使用shared_ptr智能指针访问资源不是线程安全的,需要手动加锁解锁。智能指针的拷贝也不是线程安全的。

6、请你说说C++11有什么新特性

static_assert编译时断言
新增加类型:long long,unsigned long long,char16_t,char32_t,原始字符串
auto
decltype
委托构造函数
constexpr
模板别名
alignas alignof
原子操作库
nullptr
显式转换运算符
继承构造函数
变参数模板
列表初始化
右值引用
Lambda表达式
override
final
unique_ptr、shared_ptr
初始化列表
array、unordered_map、unordered_set
线程支持库

7、说一说STL中有哪些常见的容器

  • 顺序容器

容器并非排序的,元素的插入位置同元素的值无关,包含vector、deque、list
vector
时间复杂度:插入o(n),查找o(1),删除o(n)
动态数组,元素在内存连续存放。随机存取任何元素都能在常数时间完成。在尾端增删元素具有较佳的性能。
一般2倍扩容,也有1.5倍扩容
若size>capacity,寻找一块新的空间,将原来的对象拷贝到新的内存空间。释放旧空间。内部有3个数据成员(三个指针,指向数组头部,尾指向现有元素的尾后,指向整个capacity容量大小的尾后)
扩容优化:定义元素的移动操作,采用deque(分段缓冲区)来存储
提前扩容:避免频繁扩容
迭代器失效问题
1、size>capacity,所有迭代器都失效
2、size

deque
插入o(n) 查找o(1),删除o(n)
双向队列,元素在内存连续存放。随机存取任何元素都能在常数时间完成(仅次于vector)。在两端增删元素具有较佳的性能。
分段缓冲(分段数组),可以头部插入、尾部插入,vector存储指针,一个指针指向一个定长数组(按照哈希函数,计算元素的索引,同一个索引位置有多个元素)

list
插入o(1) 查找o(n),删除o(1)
双向链表,本质上是环形链表。元素在内存不连续存放。在任何位置增删元素都能在常数时间完成,不支持随机存取。

  • 关联式容器

元素是排序的;插入任何元素,都按相应的排序规则来确定其位置;在查找时具有非常好的性能;通常以平衡二叉树(红黑树)的方式实现,包含set、multiset、map、multimap。
插入o(logn) 查找o(logn),删除o(logn)
set中不允许相同元素,multiset中允许存在相同元素。
map中存放的元素有且有两个成员变量,first、second。map根据first值对元素从小到大排序,并可快速地根据first来检索元素。map与multimap的不同在于是否允许相同first值的元素。
unordered_set、unordered_map:哈希
插入o(1) ,最坏情况:o(n)
查找o(1) ,最坏情况:o(n),
删除o(1) ,最坏情况:o(n)
-容器适配器
封装一些基础容器,使之具备新的函数功能,包含stack、queue、priority_queue
stack
后进先出,删除、检索、修改的只能是栈顶元素

queue
队列 插入只可以在尾部进行,删除、检索和修改只允许从头部进行,先进先出

priority_queue
优先队列,内部维护某种有序,然后确保优先级最高的元素总是位于头部,最高优先级元素总是第一个出列,默认大顶堆,底层默认vector

8、请你说说STL中容器的类型,每种分别有哪些容器

  • 序列式容器

array、vector、deque、list、forward_list

  • 关联式容器

map、multimap、set、multiset

  • 无序关联式容器

unordered_map、unordered_multimap、unordered_set、unordered_multiset

  • 容器适配器

stack、deque、priority_queue

9、请你说说指针和引用的区别

1、定义和性质不同
指针是一种数据类型,用于保存地址类型的数据,而引用是变量的别名。指针定义格式:数据类型 *;引用的定义格式为:数据类型&
2、引用不可以为空,当被创建的时候必须初始化,而指针变量可以是控制,在任何时候初始化
3、指针可以有多级,但引用只能是一级
4、引用使用时无需解引用,指针需要解引用
5、指针变量的值可以是NULL,而引用的值不可以是NULL
6、指针的值在初始化后可以改变,即指向其他的存储单元,而引用在进行初始化后就不会再改变
7、sizeof引用得到的是所指向的变量(对象)的大小,而sizeof指针得到的是指针变量本身的大小
8、指针作为函数参数传递时传递的是指针变量的值,而引用作为函数参数传递时传递的是实参本身,而不是拷贝副本。
9、指针和引用进行++运算意义不一样。

10、简述一下C++的重载和重写

重载

  • 不同的函数使用相同的函数名,但是函数的参数个数或类型不同(参数列表不同)。调用的时候根据函数的参数来区别不同的函数,函数重载跟返回值无关。
  • 重载的规则:函数名相同;必须具有不同的参数列表;可以有不同的访问修饰符
  • 重载用来实现静态多态(函数名相同,功能不一样)
  • 重载是多个函数或者同一个类中方法之间的关系,是平行关系

重写

  • 重写(也叫覆盖)是指派生类中重新对基类中的虚函数重新实现。函数名和参数都一样,只是函数的实现不一样。
  • 重写规则:方法声明必须完全与父类中被重写的方法相同;访问限制符的权限要大于或者等于父类中被重写的方法的访问修饰符;子类重写的方法可以加virtual,也可以不加。父类需要重写的方法要加上virtual关键字。
  • 重写用来实现动态多态(根据调用方法的对象的类型来执行不同的函数)
  • 重写是父类和子类之间的关系,是垂直关系。
  • 虚函数实现原理是采用虚函数表,多态中每个对象内存中都会有一个指针,被称为虚函数指针,这个指针指向虚函数表,表中记录的是该类的所有虚函数的入口地址,所以对象能够根据它自身的类型调用不同的函数。

11、请你说说重载、重写、隐藏的区别

重载
在同一作用域中,同名函数的形式参数(参数个数、类型或顺序)不同时,构成函数重载,与返回值类型无关
重写
派生类中与基类同返回值、同名和同参数列表的虚函数重定义,构成虚函数覆盖,也叫虚函数重写。
隐藏

  • 不同作用域中定义的同名函数构成隐藏(不要求函数返回值和函数参数类型相同)。比如派生类成员函数隐藏与其同名的基类成员函数、类成员函数隐藏全局外部同名函数。
  • 隐藏的实质:在函数查找时,名字查找先于类型检查。如果派生类中成员与基类中的成员同名,就隐藏掉。编译器首先在相应作用域中查找函数,如果找到名字则停止查找。
    C++八股_第1张图片

12、简述一下虚函数的实现原理

虚函数作用
实现动态多态机制。父类指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数,让父类的指针有”多种形态“。泛型技术
虚函数实现原理
编译器处理虚函数时,给每个对象添加一个隐藏成员(一个指针类型的数据),指向的是函数地址数组,这个数组被称为虚函数表。虚函数表中存储的是类中虚函数的地址。如果派生类重写了基类中的虚函数,则派生类对象的虚函数表中保存的是派生类的虚函数地址。如果派生类没有重写基类中的虚函数,则派生类对象的虚函数表中保存的是父类的虚函数地址。
使用虚函数时,对于内存和执行速度方面会有一定的成本:

  • 每个对象都会变大,变大的量为存储虚函数表指针
  • 对于每个类,编译器都会创建一个虚函数表
  • 对于每次调用虚函数,都需要额外指向一个操作,就是到表中查找虚函数地址

13、说一说C++和C中struct的区别以及和class的区别

C++和C中struct的区别

  • C的结构体不允许有函数存在,C++的结构体允许有内部成员函数,并且允许该函数是虚函数
  • C的结构体内部成员不能加权限,默认是public,而C++的结构体内部成员权限可以是public、protected、private,默认是public
  • C的结构体是不可以继承,C++的结构体可以从其他的结构体或者类继承
  • C中结构体不难直接初始化数据成员,C++中可以
  • C中使用结构体需要加上struct关键字,或者对结构体使用typedef取别名后直接使用,而C++中结构体可以省略struct关键字直接使用

struct和class的区别

  • struct一般用于描述一个数据结构集合,而class是对一个对象数据的封装
  • struct中默认访问控制权限是public,而class中默认的访问控制权限是private
  • 在继承关系中,struct默认是公有继承,而class默认是私有继承
  • class关键字可以用于定义模板参数,而struct不能

14、请你说说各数据类型sizeof是多少,sizeof指针是多少,sizeof原理

各数据类型sizeof是多少
bool 1 char 1 short 2 int 4 long 4 long long 8
sizeof指针是多少
32位平台下是4字节,在64位平台下是8字节
sizeof原理
sizeof是在编译的时候,查找符号表,判断类型,然后根据基础类型来取值。如果sizeof运算符的参数是一个不定长数组,则还需要在运行时计算数组长度。

15、为什么将析构函数设置成虚函数

防止内存泄漏
概念
虚析构函数,将基类的析构函数声明为virtual

class Base{
public:
	Base(){}
	virtual ~Base(){}
}

作用
防止内存泄漏。如果基类中的构造函数没有声明为虚函数,基类指针指向派生类对象时,则当基类指针释放时不会调用派生类对象的析构函数,而是调用基类的析构函数,如果派生类析构函数中做了某些释放资源的操作,则这时会造成内存泄漏。

16、请你说说导致哈希冲突的原因和影响因素,哈希冲突的解决方法

哈希冲突的原因
按照哈希函数,计算元素的索引,同一索引位置有多个元素
哈希是通过对数据进行再压缩,提高效率的一种解决方法。但由于通过哈希函数产生的哈希值是有限的,而数据可能比较多,导致经过哈希函数处理后仍然有不同的数据对应相同的值,产生哈希冲突。
产生哈希冲突的影响因素
装填因子(数据总数/哈希表长)、哈希函数、处理冲突的方法
哈希冲突的解决方法

  • 开放地址方法
  • 链式地址法(开链法):数组位置存储指针,指针指向链表,冲突位置以链表方式存储冲突的元素
  • 建立公共溢出区
  • 再哈希法(用其他哈希函数算出它的索引位置)
  • 线性探测(以冲突元素为基础往下找第一个没有放元素的位置)

哈希扩容
哈希表维护元素过多,链表长度/维护数组长度>1需进行哈希扩容,一般也是两倍扩容,4条链表->8条链表,将元素重新用哈希函数计算索引,重新放在相应链表中进行管理

17、说一说vector和list的区别,分别适用于什么场景

区别

  • vector底层实现是动态数组,list底层实现是双向链表
  • vector支持随机访问,list不支持
  • vector是顺序内存,list不是
  • vector在中间节点进行插入删除会导致内存拷贝,list不会
  • vector一次性分配好内存,不够时才进行扩容,list每次插入新节点都会进行内存申请
  • vector随机访问性能好,插入删除i性能差;list随机访问性能差,插入删除性能好

适用场景

  • vector拥有一段连续的内存空间,因此支持随机访问,如果需要高效的随机访问,而不在乎插入和删除的效率,使用vector
  • list拥有一段不连续的内存空间,如果需要高效的插入和删除,而不关心随机访问,应使用list

18、请你说说map、unordered_map的区别

1、导入的头文件不同

#include //map
#include //unordered_map

2、原理及特点

  • map内部实现一个红黑树,该结构具有自动排序的功能,因此map内部的所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行查找、删除、添加等一系列的操作都相当于是对红黑树进行这样的操作,红黑树的效率决定map的效率,时间复杂度o(logn);
  • unordered_map内部实现一个哈希表,因此其元素的排列顺序是杂乱的,无序的

19、请你说说volatile可以和const同时使用嘛

可以一起使用

  • volatile限定符用来告诉计算机,所修饰的变量的值随时都会进行修改的。用于防止编译器对该代码进行优化。编译器在用到这个变量时必须每次都小心的从内存中重新读取这个变量的值,而不是使用保存在寄存器里的备份
  • volatile是防止编译器对该代码进行优化,这个值可能变掉
  • const的含义是在代码中不能对该变量进行修改,与volatile不冲突

20、请你说说红黑树的特性,为什么要有红黑树

原因
平衡树解决了二叉查找树退化为近似链表的特点,能够把查找时间控制在o(logn),不过也不是最佳的,因为平衡树要求每个节点的左子树和右子树的高度差至多等于1,这个要求实在是太严了,导致每次进行插入和删除节点的时候,几乎都会破坏平衡树的第2个规则,我们都需要通过左旋和右旋来进行调整,使之再次成为一颗符合要求的平衡树。如果在插入、删除很频繁的场景中,平衡树需要频繁着进行调整,这会使平衡树的性能大打折扣,为了解决这个问题,有了红黑树。
红黑树特点

  • 具有二叉查找树的特点
  • 根节点是黑色的
  • 每个叶子节点都是黑色的空节点(NIL),叶子节点不存数据
  • 任何相邻的节点都不能同时为红色,红色节点是被黑色节点隔开的
  • 每个节点,从该节点到达其他可达的叶子节点的所有路径,都包含相同数目的黑树节点

C++八股_第2张图片

21、说一说static关键字的作用

static是一个关键字,可以用来修饰局部变量、全局变量、成员变量、函数、和成员方法。主要作用有:限制数据的作用域、延长数据的生命周期、修饰成员可以被该类所有对象共享。

  • 限制数据的作用域(隐藏)

所有没有加static的全局变量和函数都具有全局可见性,其他源文件中也可以访问。被static修饰的全局变量和函数只能在当前源文件中访问,其他源文件访问不了,利用这个特性可以在不同的文件中定义同名变量和同名函数,而不必担心命名冲突。

  • 延长数据生命周期

普通的局部变量出了作用域就会释放,而静态变量存储在静态区,直到程序运行结束才会释放。

  • 静态成员被该类所有对象共享

static关键字可以修饰类中成员变量和成员方法,静态成员变量和静态成员方法,静态成员拥有一块单独的存储区,不管创建多少个该类的对象,所有对象都共享这一块内存。静态成员本质上属于类,可以通过类名直接访问。

静态变量默认初始化值为0,如果没有显示初始化静态变量或者初始化为0的静态变量会存储在BSS段,而显示初始化的静态变量存储在DATA段。
静态成员函数不能访问普通成员变量,只能访问静态成员变量,并且在静态成员函数中没有this指针。

22、说一说什么是野指针,怎么产生的,如何避免

未知内存、未初始化、置为nullptr

野指针
野指针是指指向的位置是随机的、不可知的、不正确的

产生原因

  • 指针变量未初始化或者随便赋值:指针变量没有初始化,其值是随机的,也就是指针变量指向的是不确定的内存,如果对它解引用,结果是不可知的
  • 指针释放后未置空:有时候指针在释放后没有赋值为nullptr,虽然指针变量指向的内存被释放掉了,但指针变量的值还在,这时指针变量就是一个未知的内存,如果对他解引用,结果是不可知的
  • 指针操作超过了变量的作用域:函数中返回了局部变量的地址或者引用,因为局部变量出了作用域就释放了,这时返回的地址指向的内存也是未知的

如何避免野指针

  • 指针变量一定要初始化,可以初始化为nullptr,因为nullptr明确表示为空指针,对nullptr操作不会有问题
  • 内存释放后指针置为nullptr

23、说说const和define的区别

作用、编译阶段、预处理阶段、简单替换、类型检查、内存
const在C语言中表示只读,编译器禁止对它修饰的变量进行修改,在C++中增加了常量的语义。而define用于定义宏,而宏也可以用于定义常量。
区别:

  • const生效于编译阶段,而define生效于预处理阶段
  • define只是简单的字符串替换,没有类型检查,而const有对应的数据类型,编译器要进行判断的,可以避免一些低级的错误
  • 用define定义的常量是不可以用指针变量去指向的,用const定义的常量是可以用指针去指向该常量的地址的
  • define不分配内存,给出的是立即数,有多少次使用就进行多少次替换,在内存中会有多个拷贝,消耗内存大,const在静态存储区中分配空间,在程序运行时内存中只有一个拷贝
  • 可以对const变量进行调试,但是不能对宏常量进行调试。

24、请你说说vector和扩容机制,扩容以后,它的内存地址会变化吗

申请空间、拷贝数据、释放旧空间
当vector的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么vector就需要扩容。vector容器扩容的过程分为3步:

  • 1、完全弃用现有的内存空间,重新申请更大的内存空间
  • 2、将旧内存空间中的数据,按原有顺序移动到新的内存空间
  • 3、最后将旧的内存空间释放,

因为vector扩容需要申请新的空间,所以扩容以后它的内存地址会发送改变。vector扩容是非常耗时的,为了降低再次分配内存空间时的成本,每次扩容时vector都会申请比用户需求量更多的内存空间(vector的容量capacity>size),以便后期使用。

一般2倍扩容,也有1.5倍扩容。
扩容优化:定义元素的移动操作,采用deque(分段缓冲区)来存储
提前扩容:避免频繁扩容

25、请你说说unordered_map实现原理

哈希表、链地址法
unordered_map容器和map容器一样,以键值对(pair类型)的形式存储数据,存储的各个键值对的键互不相同且不允许被修改。

但由于unordered_map容器底层采用的是哈希表存储结构,该结构本身不具有对数据的排序功能,所以此容器内部不会自行对存储的键值对进行排序。

底层采用哈希表实现无序容器时,会将所有数据存储到一整块连续的内存空间中,并且当数据存储位置发生冲突时,解决方法选用的是”链地址法(开链法)”。
C++八股_第3张图片
当使用无序容器存储键值对时,会先申请一整块连续的存储空间,但此空间并不用来直接存储键值对,而是存储各个链表的头指针,各键值对真正的存储位置是各个链表的节点。

C++STL标准库中,将图中的各个链表称为桶(bucket),每个桶都有自己的编号(从0开始)。当有新键值对存储到无序容器中时,整个存储过程分为如下几步:

  • 1、将该键值对中键的值代入设计好的哈希函数,得到一个哈希值(一个整数,用H表示)
  • 2、将H和无序容器拥有桶的数量n做整除运算(H%n),该结果即表示应将此键值对存储到的桶的编号
  • 3、建立一个新节点存储此键值对,同时将该节点链接到相应编号的桶上

哈希表存储结构的一个重要属性:负载因子(load factor)。该属性同样适用于无序容器,用于衡量容器存储键值对的空/满程度,负载因子越大,意味着容器越满,即各链表中挂载着越多的键值对,会降低容器查找目标键值对的效率;反之,负载因子越小,容器越空,但并不一定各个链表中挂载的键值对就越少。

如果设计的哈希函数不合理,使得各个键值对的键代入该函数得到的哈希值始终相同(所有键值对始终存储在同一链表上)。这种情况下,即便增加桶数使得负载因子减小,该容器的查找效率依旧很差。

无序容器中,负载因子的计算方式:
负载因子=容器存储的总键值对/桶数

默认情况下,无序容器的最大负载因子为1.0。如果操作无序容器过程中,使得最大负载因子超过了默认值,则容器会自动增加桶数,并重新进行哈希,以此减小负载因子。此过程会导致容器迭代器失效,但指向单个键值对的引用或者指针仍然有效。这也解释了为啥我们在操作无序容器过程中,键值对的存储顺序有时会莫名的发生变动。

26、请你说说unique_ptr的实现原理及使用场景

所有权、private、delete
实现原理
建立所有权(ownership)概念,对于特定的对象,只能有一个智能指针可拥有它,这样只有拥有对象的智能指针的析构函数会删除该对象。然后,让赋值操作转让所有权。

unique_ptr中把拷贝构造函数和拷贝赋值运算符声明为private或delete。这样就不可以对指针指向进行拷贝了,也不会产生指向同一个对象的指针。

使用场景

  • 如果程序不需要多个指向同一个对象的指针,则可以使用unique_ptr;
  • 如果使用new[]分配内存,应该选择unique_ptr
  • 如果函数使用new分配内存,并返回指向该内存的指针,将其返回类型声明为unique_ptr是不错的选择

27、请你说说C++Lambda表达式用法及实现原理

sort(v.begin(),v.end(),[](int a,int b)->bool{return a<b;});

Lambda表达式:
[外部变量访问方式说明符](参数)mutable noexcept/throw() ->返回值类型{函数体;}

1、【外部变量访问方式说明符】捕获列表
[]方括号用于向编译器表明当前是一个Lambda表达式,其不能被省略。在方括号内部,注明当前Lambda函数的函数体可以使用哪些“外部变量”。

外部变量指的是和当前lambda表达式位于同一作用域的所有局部变量。
[外部变量]的定义方式:

  • []:空方括号表示当前lambda匿名函数不导入任何外部变量
  • [=]:表示以值传递的方式导入所有外部变量
  • [&]:表示以引用传递方式导入所有外部变量
  • [val1,val2,…]:表示以值传递的方式导入val1、val2等指定的外部变量,同时多个变量之间没有前后次序
  • [&val1,&val2,…]:表示以引用传递的方式导入val1、val2等指定的外部变量,多个变量之间没有前后次序
  • [val,&val2,…]:值传递和引用传递方式混合使用,变量之间没有前后次序
  • [=,&val1,…]:表示除val1以引用传递的方式导入外,其他外部变量都以值传递的方式导入
  • [this]:表示以值传递的方式导入当前this指针

2、(参数)参数列表
和普通函数的定义一样,Lambda匿名函数也可以接收外部传递的多个参数,和普通函数不同的是,如果不需要传递参数,可以连同()小括号一起省略

3、mutable
此关键字可以省略,如果使用则之前的()小括号将不能省略(参数个数可以为0)。默认情况下,对于以值传递方式引入的外部变量,不允许在Lambda表达式内部修改它们的值(可以理解为这部分变量都是const变量)。而如果想修改它们,就必须使用mutable关键字。对于以值传递方式引入的外部变量,Lambda表达式修改的是拷贝的那一份,并不会修改真正的外部变量。

4、noexcept/throw()
可以省略,如果使用,在这之前的()小括号将不能省略(参数个数可以为0)。默认情况下,Lambda函数的函数体可以抛出任何类型的异常。而标注noexcept关键字,则表示函数体内不会抛出任何异常;使用throw()可以指定Lambda函数内部可以抛出的异常类型。

5、 ->返回值类型
指明Lambda匿名函数的返回值类型。如果Lambda函数体只有一个return语句,或者该函数返回void,则编译器可以自行推断返回值类型,此情况下可以直接省略->返回值类型

6、函数体
和普通函数一样,Lambda匿名函数包含的内部代码都放置在函数体中。该函数体内除了可以使用指定传递进来的参数之外,还可以使用指定的外部变量以及全局范围内的所有全局变量

编译器实现Lambda表达式步骤:

  • 1、创建一个未命名的类,实现构造函数,使用Lambda表达式的函数体重载operator()(Lambda表示也叫匿名函数对象)
  • 创建未命名的类的对象
  • 通过对象调用operator()

28、什么是纯虚函数,有什么作用

概念
纯虚函数是一种特殊的虚函数,它的格式是:虚函数不给出具体的实现,也就是后面没有大括号实现体,而在后面加上“=0

class 类名{
	virtual 返回值类型 函数名(参数列表)=0;
};

作用
很多情况下,在基类中不能对虚函数给出具体的有意义的实现,就可以把它声明为纯虚函数,它的实现留给该基类的派生类去做。
例如猫类和狗类的基类是动物类,动物类中有一个吃饭的函数eat(),那这个eat()函数可以是纯虚函数,因为并不能够确定动物吃的东西是什么,具体吃的内容由不同的派生类去实现。

特点
如果一个类中有纯虚函数,那么这个类也被称为抽象类。这个类不能实例化对象,也就是不能创建该类的对象。除非在派生类中完全实现基类中的所有纯虚函数,否则派生类也是抽线类,不能实例化对象。

29、请你说说虚函数和纯虚函数的区别

定义格式

//虚函数
virtual 返回值类型 函数名(参数列表){
  //函数体
}

//纯虚函数
virtual 返回值类型 函数名(参数列表)=0;

特点
虚函数可以有具体的实现,纯虚函数没有具体的实现。对于虚函数来说,父类和子类都有各自的版本,由多态方式调用的时候动态绑定。

有纯虚函数的类称为抽象类,有纯虚函数的类不能实例化,派生类必须实现纯虚函数才可以实例化,否则也是抽象类。

作用
虚函数是C++中用于实现动态多态的机制。很多情况下,在基类中不能对虚函数给出具体的有意义的实现,就可以把它声明为纯虚函数,它的实现留给该基类的派生类去做。

30、简述vector的实现原理

动态数组、连续存储空间、扩容

vector是一种动态数组,在内存中具有连续的存储空间,支持快速随机访问,由于具有连续的存储空间,所以在插入和删除操作方面,效率比较慢。

当vector的大小和容量相等(size==capacity)也就是满载时,如果再向其添加元素,那么vector就需要扩容。
vector容器扩容的过程需要经历以下步骤:

  • 完全弃用现有的内存空间,重新申请更大的内存空间
  • 将旧内存空间中的数据,按原有顺序移动到新的内存空间中
  • 最后将旧的内存空间释放

vector扩容是非常耗时的,为了降低再次分配内存空间时的成本,每次扩容时vector都会申请比用户需求量更多的内存空间(capacity>size),2倍扩容也有1.5倍扩容,以便后期使用。

31、请你说说deque的实现原理

分段连续内存、中控器
deque是由一段一段的定量的连续空间构成。一旦有必要在deque前端或尾端增加新的空间,便配置一段连续定量的空间,串接在deque的头端或尾端。

deque最大的工作就是维护这些分段连续的内存空间的整体性的假象,并提供随机存取的接口,避开了重新配置空间,复制,释放的轮回,代价就是复杂的迭代器架构。

deque是分段连续内存空间,有中央控制维持整体连续的假象,数据结构的设计及迭代器的前进后退操作颇为繁琐。
deque采用一小块连续的内存空间作为主控,其中每一个元素(结点)都是一个指针,指向另一段连续内存空间(缓存区),缓冲区才是deque的存储空间的主体。
C++八股_第4张图片

32、请你说说map实现原理,各操作的时间复杂度是多少

红黑树
1、map实现原理
map内部实现了一个红黑树(红黑树是非严格平衡的二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树有自动排序功能,因此map内部所有元素都是有序的,红黑树的每一个结点都代表着map的一个元素。因此对map进行的查找、删除、添加等一系列的操作相当于是对红黑树进行的操作。

map中的元素是按照二叉树(二叉查找树、二叉排序树)存储的,左子树上所有结点的键值都小于根结点的键值,右子树所有的键值都大于根节点的键值,使用中序遍历可将键值按照从大到小遍历出来。

2、各操作的时间复杂度
插入、查找、删除的时间复杂度均为:o(logn)

33、shared_ptr怎么知道跟它共享对象的指针释放了

引用计数

shared_ptr底层是采用引用计数的方式实现的。引用计数器,用于记录多少个shared_ptr共享同一个对象。每当创建一个新的shared_ptr对象时,该计数器就会加1,当shared_ptr对象被销毁时,计数器就会减1.当计数器的值变为0时,表示没有任何shared_ptr对象引用该对象,此时shared_ptr会自动释放该对象的内存。这种机制可以避免内存泄漏和悬空指针等问题。

34、请你说说extern的作用,extern变量在哪个数据段,为什么要extern C

声明外部变量和函数、静态存储区(全局区)、BSS、DATA、C/C++混合开发

  • extern可以置于变量声明或者函数声明前,以表示变量或者函数的定义在别的文件中,提示编译器遇到此变量和函数时在其他文件中寻找其定义
  • extern变量表示声明一个变量,表示该变量是一个外部变量,也就是全局变量,所以extern修饰的变量保存在静态存储区(全局区),全局变量如果没有显示初始化,会默认初始化为0,或者显示初始化为0,则保存在程序的BSS段,如果初始化不为0,则保存在程序的DATA段。
  • extern "C"的作用是为了能够正确的实现C++代码调用C语言代码,指示编译器这部分代码按照C语言而不是C++语言的方式进行编译。

由于C++支持函数重载,编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。

C++出现之前,很多代码都是C语言写的,而且很多底层的库也是C语言写的,为了更好的支持原来的C代码和已经写好的C语言库,需要在C++中尽可能地支持C,**extern"C"**就是其中一个策略。

35、请你说说set的实现原理

红黑树、排序

set底层使用红黑树实现,一种高效的平衡检索二叉树。set容器中每一个元素就是二叉树的每一个节点,对于set容器的插入删除操作,效率都比较高,原因是二叉树的删除插入元素并不需要进行内存拷贝和内存移动,只是改变了指针的指向。

对set进行插入删除操作都不会引起迭代器的失效。因为迭代器相当于一个指针指向每一个二叉树的节点,对set的插入删除并不会改变原有内存中的节点。

set中的元素都是唯一的,而且默认情况下会对元素进行升序排列。
不能直接改变元素值,因为那样会打乱原本正确的顺序,要改变元素值必须先删除旧元素,再插入新元素。
不提供直接存取元素的任何操作函数,只能通过迭代器进行间接存取。

36、请你回答一下智能指针有没有内存泄漏的情况

shared_ptr、循环引用、weak_ptr
当两个类对象中各自有一个shared_ptr指向对方时,会造成循环引用,使引用计数失效,从而导致内存泄漏。

为了解决循环引用导致的内存泄漏,引入弱指针weak_ptr。weak_ptr的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但是不会指向引用计数的共享内存,但是可以检测到所管理的对象是否已经被释放,从而避免非法访问。

37、请你说说左值、右值、左值引用、右值引用的使用场景

1、左值
C++中可以取地址的、有名字的就是左值
2、右值
不能取地址的、没有名字的就是右值

int a=10;//a就是左值,10就是右值

3、左值引用
对一个左值进行引用。
传统的C++引用(左值引用)使得标识符关联到左值。左值是一个表示数据的表达式(如变量名或解除引用的指针),程序可以获取其地址。

最初,左值可出现在赋值语句的左边,但修饰符const的出现使得可以声明这样的标识符,即不能给他赋值,但可获取其地址:

int n;
int *pt=new int;
const int b=101;
int &rn=n;
int &rt=*pt;
const int &rb=10;

4、右值引用
右值引用就是对一个右值进行引用
C++11新增了右值引用(rvalue reference)。这种引用可指向右值(即出现在赋值表达式右边的值),但不能对其应用地址运算符。右值包括字面常量、诸如x+y等表达式以及返回值的函数(条件是该函数返回的不是引用),右值引用使用&&声明

int x=10;
int y=23;
int &&r1=13;
int &&r2=x+y;
double &&r3=sqrt(2.0);

5、右值引用的使用场景
右值引用可以实现移动语义、完美转发

38、说说C语言和C++语言的区别

  • C语言是面向过程的语言,而C++支持面向对象(封装、继承、多态)
  • C++支持函数重载,C语言不支持
  • C程序中如果函数没有任何参数需要将参数定义为void以此限定函数不可传递任何参数,如果不进行限定让参数表默认为空其意义是可以传递任何参数;在C++中,不带参数的函数表示函数不能传递任何参数
  • C语言struct中不能有函数,而C++语言struct中可以有函数
  • C语言函数参数不支持默认值,而C++语言支持参数默认值
  • C++语言支持内联函数,而C语言不支持
  • C++支持引用,C语言不支持
  • C语言采用malloc和free函数动态申请和释放内存,而C++使用new和delete运算符
  • C语言中只有局部和全局两个作用域,而C++中有局部、全局、类、名称空间作用域

39、简述一下C++中的4种类型转换

使用C风格的类型转换可以把想要的任何东西转换成我们需要的类型,但是这种类型转换太过松散。C++提供了更严格的类型转换,可以提供更好的控制转换过程,并添加4个类型转换运算符,使转换过程更规范:
1、static_cast静态转换

  • 用于类层次结构中基类(父类)和派生类(子类)之间指针和引用的转换;
  • 进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;
  • 进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的;
  • 用于基本数据类型之间的转换,如把int转换成char,把char转换成int。这种转换的安全性也要开发人员来保证。

2、dynamic_cast动态转换

  • 用于类层次间的上行转换和下行转换
  • 在类层次间进行上行转换时,dynamic_cast和static_cast的效果是一样的
  • 在进行下行转换时,dynamic_cast具有类型检查功能,比static_cast更安全

3、const_cast常量转换

  • 该运算符用来修改类型的const属性,常量指针被转换成非常量指针,并且仍然指向原来的对象,常量引用被转换成非常量引用,并且指向原来的对象。
  • 不能直接对非指针和非引用的变量使用const_cast操作符

4、reinterpret_cast重新解释类型转换

  • 这是最不安全的一种转换机制,最有可能出问题。
  • 主要用于将一种数据类型转换成另一种数据类型,它可以将一个指针转换成一个整数,也可以将一个整数转换成一个指针

40、weak_ptr如何解决shared_ptr的循环引用问题

引用计数

  • weak_ptr是为了配合shared_ptr而引入的一种智能指针,它指向一个由shared_ptr管理的对象而不影响所指对象的生命周期,将一个weak_ptr绑定到一个shared_ptr不会改变shared_ptr的引用计数,依次特性可以解决shared_ptr的循环引用问题。
  • weak_ptr没有解引用*和获取指针->运算符,它只能通过lock成员函数去获取对应的shared_ptr智能指针对象,从而获取对应的地址和内容。
  • 不论是否有weak_ptr指向,一旦最后一个指向对象的shared_ptr被销毁,对象就会被释放。

41、请你说说const的用法

变量、指针、函数参数、成员、成员方法
1、用在变量身上
表示变量只读,不能对它的值进行修改

const int a=10;
a=20;//编译会报错,因为a只读,不能对它进行修改

2、结合指针一起使用

const int* p;//底层cosnt,表示指针变量p所指向的内容不能修改,指针变量p的内容可以修改
int * const p;//顶层const,表示指针变量p的指向不能修改,指针变量p所指向的内容可以修改
const int * const p;//指针p指向和所指向的内容都不可以修改

3、const用于函数参数

void foo(const int* p);
void foo(const int& p);

const用于形参时说明形参在函数内部不能被改变,有时候函数参数传递指针或者引用,在函数内部不希望对指针和引用指向的数据进行修改,可以加上const
4、在类中修饰成员方法
防止在方法中修改非static成员

class A{
public:
	int a;
	void func()const{
		a=20;//错误,const修饰的成员方法中不能修改非静态成员变量
	}
};

this指针只作用在非静态成员函数的隐式形参里。
5、const修饰类的成员变量

class T{
public:
	T():a(10);
private:
	const int a;
	static const int b;
};

const int T::b=20;

类的成员变量可以分为静态和非静态的,如果const修饰的是非静态成员变量,可以在构造函数中对该变量进行初始化;如果const修饰的是静态成员变量,则需要在类外对该变量进行初始化。

42、请你说说C++引用的概念

  • 引用(Reference)是C++相对于C语言的一个扩充。引用可以看作是数据的一个别名,通过这个别名和原来的名字都能能够找到这份数据。
  • 引用类似于Windows中的快捷方式,一个可执行程序可以有多个快捷方式,通过这些快捷方式和可执行程序本身都能够运行程序;引用还类似于人的绰号,使用绰号和本名都能表示一个人
typename &ref=varname;
  • 引用必须引用合法的内存空间
  • 引用在定义时必须初始化
  • 引用一旦初始化后,就不能再引用其他数据
  • 引用在定义时需要加上&,在使用时不加&,使用时加&表示取地址
  • 函数中不要返回局部变量的引用
  • 引用的本质是指针,底层的实现还是指针

43、说说内联函数和函数的区别,内联函数的作用

inline、函数调用开销、寻址、展开代码、提高效率、宏定义

1、内联函数和函数的区别

  • 内联函数比普通函数多了关键字inline
  • 内联函数避免了函数调用的开销,普通函数有调用的开销
  • 普通函数的被调用的时候,需要寻址(函数入口地址),内联函数不需要寻址
  • 内联函数有一定的限制,内联函数要求代码简单,不能包含复杂的结构控制语句,如果内联函数函数体过于复杂,编译器将自动把内联函数当成普通函数来执行;普通函数没有这个要求

2、内联函数的作用
因为函数调用时需要创建时间、参数传入等操作,造成时间和空间的额外开销。通过编译器预处理,在调用内联函数的地方将内联函数内的语句复制到调用函数的地方,也就是直接展开代码执行,从而提高了效率,减少了一些不必要的开销。同时内联函数还能解决宏定义的问题。

44、请你说说虚函数可以是内联函数嘛

  • 虚函数可以是内联函数,内联是可以修饰虚函数的,但是当虚函数表现多态性的时候不能内联
  • 内联是在编译期建议编译器内联,而虚函数的多态性是在运行期,编译器无法知道运行期调用哪个代码,因此虚函数表现为多态性时不可以内联
  • inline virtual唯一可以内联的时候是:编译器知道所调用的对象是哪个类,这只有在编译器具有实际对象而不是对象的指针或引用时才会发生

45、请你说说迭代器失效原因,有哪些情况

  • STL中某些容器调用了某些成员方法后会导致迭代器失效。例如,vector容器,如果调用reserve()来增加容器容量,之前创建好的任何迭代器(开始迭代器和结束迭代器)都可能会失效,为了增加容器的容量,vector容器的元素可能被复制或移动到了新的内存地址
  • 序列式容器迭代器失效,如vector、deque,序列式容器是组合式容器,当当前元素的迭代器被删除后,其后的所有元素的迭代器都会失效,这是因为vector、deque都是连续存储的一段空间,当对其进行erase操作时,其后的每一个人元素都会向前移一个位置。解决:erase返回下一个有效的迭代器。
  • 关联式容器迭代器失效如map、set,删除当前的迭代器,仅仅会使当前的迭代器失效,只要在erase时,递增当前迭代器即可。因为map之类的容器,使用了红黑树来实现,插入、删除一个节点不会对其他节点造成影响。erase迭代器只是被删元素的迭代器失效,但是返回值是void,所以采用erase(iter++)自增方式删除迭代器。

46、请你说说auto和decltype如何使用?

C++11提供了多种简化声明的功能,尤其在使用模板时。

  • auto实现自动类型推断,要求进行显示初始化,让编译器能够将变量的类型设置为初始值的类型
auto a=12;
auto pt=&a;
double fm(double a,int b){
	return a+b;
}
auto pf=fm;
//简化模板声明
for(std::initializer_list<double>::iterator p=il.begin();p!=il.end();p++)
for(auto p=il.begin();p!=il.end();p++) 
  • decltype将变量的类型声明为表达式指定的类型
decltype(expression) var;
decltype(x) y;//让y的类型和x相同,x是一个表达式
double x;
int n;
decltype(x*n) q;
decltype(&x) pd;
template<typename t="">
void ef(T t,U u){
	decltype(T*U) tu;
}
</typename></double>

47、虚析构函数有什么作用

防止内存泄漏

1、概念
虚析构函数,是将基类的析构函数声明为virtual

class Base{
public:
	Base(){}
	//虚析构函数
	virtual ~Base(){}
};

2、作用

  • 虚析构函数的主要作用是为了防止遗漏资源的释放,防止内存泄漏
  • 如果基类中的析构函数没有声明为虚函数,基类指针指向派生类对象时,则当基类指针释放时不会调用派生类对象的析构函数,而是调用基类的析构函数,如果派生类析构函数中做了某些释放资源的操作,则此时会造成内存泄漏

48、请你说说什么情况下会调用拷贝构造,什么时候会调用赋值操作

1、拷贝构造函数的调用时机

  • 用一个对象初始化另一个对象
  • 对象以值传递的方式传递给函数参数
  • 函数局部对象以值传递的方式从函数返回

2、赋值操作的调用时机

  • 将一个对象赋值给另外一个对象

49、简述一下C++11中可变参数模板新特性

C++11之前,类模板和函数模板只能含有固定数量的模板参数。C++11增强了模板功能,它对参数进行高度泛化,允许模板定义中包含0到任意个、任意类型的模板参数,这就是可变参数模板。可变参数模板的加入使得C++11的功能变动更加强大,能够很有效的提升灵活性。
1、可变参数函数模板语法
模板参数中,typename后跟…表明T是一个可变模板参数,它可以接收多种数据类型,称为模板参数包。

template<typename ...t="">
//func()函数中,args参数的类型用T...表示,表示args参数可以接收任意个参数,函数参数包
void fun(T ...args){
//函数体
}

2、可变参数类模板语法

template<typename ...types="">
class test;

3、展开参数包的方式

  • 可变参数函数模板可以采用递归方式、逗号表达式+初始化列表的方式展开参数包
  • 可变参数类模板可以采用递归+继承的方式展开参数包

50、说说C++智能指针和指针的区别

1、智能指针

  • 如果程序中使用new从堆(自由存储区)分配内存,等到不需要时,应使用delete将其释放。C++引入auto_ptr,以帮助自动完成这个过程。随后的编程体验(尤其是使用STL)表明,需要有更精致的机制。基于程序员的编程体验和BOOST库提供的解决方案,C++摒弃了auto_ptr,并新增了3种智能指针:unique_ptr、shared_ptr和weak_ptr。所有新增的智能指针都能与STL容器和移动语义协同工作。

2、指针

  • C语言规定所有变量在使用前必须先定义,指定其类型,并按此分配内存空间。指针变量不同于整型变量和其他类型的变量,它是专门用来存放地址的,所以必须将它定义为“指针类型”。

3、智能指针和普通指针的区别

  • 智能指针实际上是对普通指针加了一层封装机制,区别是它负责自动释放所指的对象,方便管理对象生命周期。
  • 指针是一种数据类型,用于保存内存地址,智能指针是类模板。

你可能感兴趣的:(c++)