目录

1.你熟悉的STL有哪些?

STL:

  • vector: 底层实现是一个顺序表结构,可以动态增长长度的数组
  • deque: 双端队列,deque的底层实现是一个链表数组,序列式容器
  • list: 底层实现是一个双向链表,序列式容器
  • map: 底层通常是由一颗红黑树组成,第一个可以称为键(key),第二个可以称为该键的值(value),在map内部所有的key都是有序的,并且不会有重复的值
  • set: 集合内的元素是不重复的 ,类似map中的key元素,但是没有对应的value

2.各排序算法的时间复杂度

  • 冒泡排序:最好的情况是数据本来就有序,复杂度为O( n n n);最差的情况是O( n 2 n^2 n2),稳定算法。

  • 选择排序:最好的情况是数据本来就有序,复杂度为O( n n n);最差的情况是O( n 2 n^2 n2),不稳定算法

  • 直接插入排序:最好的情况是数据本来就有序,复杂度为O( n n n);最差的情况是O( n 2 n^2 n2),稳定算法

  • 希尔排序:最好的情况复杂度为O( n n n);最差的情况是O( n 2 n^2 n2),但平均复杂度要比直接插入小,不稳定算法

  • 快速排序:最好的情况复杂度为 n l o g n nlogn nlogn,最差的情况是O( n 2 n^2 n2),快速排序将不幸退化为冒泡排序;不稳定(比如序列5 3 3 4 3 8 9 10 11,现在中枢元素5和3(第5个元素,下标从1开始计)交换就会把元素3的稳定性打乱)
    在这里插入图片描述
    https://www.cnblogs.com/onepixel/articles/7674659.html

  • 冒泡排序:
    1.比较相邻的元素。如果第一个比第二个大,就交换它们两个;
    2.对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
    3.针对所有的元素重复以上的步骤,除了最后一个;
    4.重复步骤1~3,直到排序完成。

  • 选择排序:
    首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

  • 插入排序:
    它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

  • 希尔排序:
    先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:
    1.选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
    2.按增量序列个数k,对序列进行k 趟排序;
    3.每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m 的子序列,分别对各子表进行直接插入排序。仅增量因子为1 时,整个序列作为一个表来处理,表长度即为整个序列的长度。

  • 归并排序:
    1.把长度为n的输入序列分成两个长度为n/2的子序列;
    2.对这两个子序列分别采用归并排序;
    3.将两个排序好的子序列合并成一个最终的排序序列。

  • 快速排序

void QuickSort(vector<int> &vec,int L,int R)
{
	if (L >= R)
	{
		return;
	}
	int left = L, right = R;
	int pivot = vec[L];
	while (left < right)
	{
		while (left < right && vec[right] > pivot)
		{
			right--;
		}
		if (left < right)
		{
			vec[left] = vec[right];
		}
		while (left < right && vec[left] <= pivot)
		{
			left++;
		}
		if (left < right)
		{
			vec[right] = vec[left];
		}
		if (left >= right)
		{
			vec[left] = pivot;
		}
	}
	QuickSort(vec, L, right - 1);
	QuickSort(vec, right + 1, R);

}
  • 堆排序:
    1.将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
    2.将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
    3.由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

  • 计数排序:
    1.找出待排序的数组中最大和最小的元素;
    2.统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
    3.对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
    4.反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1

  • 桶排序:
    假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。

  • 基数排序:
    1.取得数组中的最大数,并取得位数;
    2.arr为原始数组,从最低位开始取每个位组成radix数组;
    3.对radix进行计数排序(利用计数排序适用于小范围数的特点);

3.Eigen是行优先还是列优先?

在Eigen中矩阵的存储默认是列优先的

4.c++ 虚函数与纯虚函数的区别

C++ 虚函数和纯虚函数的区别:

  • 定义一个函数为虚函数,不代表函数为不被实现的函数。定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。
  • 定义一个函数为纯虚函数,才代表函数没有被实现。定义纯虚函数是为了实现一个接口,起到一个规范的作用,规范继承这个类的程序员必须实现这个函数。

5.map 与 unordered map 区别 ++

mapunordered map
排序方式在默认情况下,按照键递增的排序顺序无序(顺序可能是乱的,不一定是数据的输入顺序)
底层内部采用了自平衡的BST(二叉搜索树)的数据结构,实现了数据排序采用了哈希表的数据结构
搜索时间复杂度为log(n)O(1)为平均时间,最坏情况下的时间复杂度为O(n)
插入操作的时间复杂度复杂度为log(n)+再平衡时间与搜索的时间复杂度一样
删除操作的时间复杂度复杂度为log(n)+再平衡时间与搜索的时间复杂度一样
  • 如果你想要一个具有排序后的数据的话,通常可以选择Map这种类型。
  • 如果你只想记录数据而不是想要将数据进行排序的话,那么就可以选择unordered_map这种数据结构。

6.C++ 三大特性

  • 封装,继承,多态
  1. 继承
     被继承的是父类(基类),继承出来的类是子类(派生类),子类拥有父类的所有的特性。
     继承方式有公有继承、私有继承,保护继承。默认是私有继承

  2. 多态⭐
     多态性是指对不同类的对象发出相同的消息将会有不同的实现
     C++有两种多态,称为动多态(运行期多态)和静多态(编译器多态),静多态主要是通过模板来实现,而动多态是通过虚函数来实现的。即在基类中存在虚函数(一般为纯虚函数)子类通过重载这些接口,使用基类的指针或者引用指向子类的对象,就可以调用子类对应的函数,动多态的函数调用机制是执行器期才能确定的,所以他是动态的。

静多态绑定发生在编译期(静态绑定),动多态绑定发生在运行期(动态绑定)

  1. 封装
     隐藏类的属性和实现细节,仅仅对外提供接口,
     封装性实际上是由编译器去识别关键字public、private和protected来实现的,体现在类的成员可以有公有成员(public),私有成员(private),保护成员(protected)。私有成员是在封装体内被隐藏的部分,只有类体内说明的函数(类的成员函数)才可以访问私有成员,而在类体外的函数时不能访问的,公有成员是封装体与外界的一个接口,类体外的函数可以访问公有成员,保护成员是只有该类的成员函数和该类的派生类才可以访问的。

7. 重载,重写和重定义的区别

  • 重载(成员函数重载特征):作用域相同(在同一个类中),函数名相同,参数不同,返回值可以不同,virtual关键字可有可无
  • 重写(派生类函数覆盖基类函数):作用域不同(分别在基类和派生类),函数名相同,参数相同,返回值相同,基类函数必须有virtual关键字
  • 重定义(派生类的函数屏蔽了与其同名的基类函数):1. 如果派生类的函数与基类的函数同名,但是参数不同,此时不管有无virtual,基类的函数被隐藏;2.如果派生类函数与基类函数同名,参数也相同,但是基类函数没有virtual关键字,此时基类的函数被隐藏。如果有virtual,就是重写。

8. 红黑树是什么树结构?

  • 红黑树是一种弱平衡二叉查找树,在进行插入和删除操作时通过特定操作保持二叉查找树的平衡。
    其可以在O(logn)时间内查找,插入和删除,此处n是树中元素的数目。
  • map的底层实现就是红黑树

9. 变量的声明和定义有什么区别?

  • 变量的定义为变量分配地址和存储空间
  • 变量的声明不为变量分配地址。
  • 一个变量就可以在多个地方声明,但只能在一个地方定义。
  • 加入extern 修饰的是变量的声明,说明此变量将在文件以外或后面部分定义

10. 简述c++程序编译的内存分配情况

  • 从静态存储区域分配:
    内存在程序编译时就已经分配好了。如全局变量,static变量,常量字符串等;
  • 在栈上分配:
    在执行函数时,函数内局部变量的存储单元都在栈上创建,函数执行结束时这些存储单元自动释放;
  • 在堆上分配:
    即动态内存分配。程序在运行时用malloc或new申请任意大小的内存,程序员自己负责在何时用free或delete释放内存。

11.指针和引用的区别?

  1. 指针有自己的一块空间,而引用只是一个别名;
  2. 使用sizeof看一个指针的大小是4,而引用则是被引用对象的大小;
  3. 可以有const 指针,但没有const 引用;
  4. 指针在使用中可以指向其他对象,但引用不能改变;
  5. 引用必须要初始化,指针可以有空指针;

12. typedef和define的区别

  1. typedef用来定义一种数据类型的别名;define主要用来定义常量或是使用频繁的宏;
  2. typedef有作用域限制,define不受作用域约束,只要是在define声明后的引用都是正确的;
  3. typedef定义的是语句,句尾要加上分号。而define不是语句,不能在句尾加分号;

13.指针常量和常量指针区别 ++

指针常量是指这个指针的值只能在定义时初始化,其他地方不能改变。
常量指针是指这个指针指向一个只读的对象;
指针常量强调指针的不可改变性,而常量指针强调指针所指向对象的不可改变性

14.new/delete与malloc/free的区别

  • new能自动计算需要分配的内存空间,而malloc需要手动计算字节数
  • new调用构造函数,malloc不能;delete调用析构函数,而free不能

15.左值和右值

  • 左值:可变,能取地址,能赋值
  • 右值:不可变,不能取地址,不能赋值

16. c++四种cast转换

c++四种类型转换:static_cast,dynamic_cast,const_cast,reinterpret_cast

  1. const_cast
    用于将const变量转为非const
  2. static_cast
    用于各种隐式转换,比如非const转const,void*转指针等,
  3. dynamic_cast
    用于动态类型转换.只能用于含有虚函数的类
  4. reinterpret_cast
    几乎什么都可以转,比如将int转指针

为什么不使用c的强制转换 :c的强制转换不能检查错误,容易出错

17. c++ 四个智能指针

c++里面有四个智能指针: auto_ptr(已经被c++11弃用), shared_ptr, weak_ptr, unique_ptr.
智能指针就是一个类,当超过类的作用域时,类会自动调用析构函数,自动释放资源.

  • unique_ptr:保证同一时间只有一个智能指针可以指向该对象
unique_ptr<string> p3 (new string ("auto"));   
unique_ptr<string> p4;                       
p4 = p3;//此时会报错!!

C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个.

  • shared_ptr:多个智能指针可以指向相同对象,该对象和其相关资源会在"最后一个引用被销毁"时释放.
  • weak_ptr:是一种不控制对象生命周期的智能指针.weak_ptr只是提供了对管理对象的一个访问手段.

18. 谈谈你对拷贝构造函数和赋值运算符的认识

  • 拷贝构造函数生成新的类对象,而赋值运算符不能
  • 当有类中有指针类型的成员变量时,需要重写拷贝构造函数和赋值运算符,不要使用默认的.

19. 为什么析构函数一般写成虚函数? ++

由于类的多态性,如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就造成派生类对象析构不完全,造成内存泄漏,

  • 所以注意如果基类不是虚析构函数的话可能会有以下两点问题:

1、子类所分配的内存不能被释放

2、子类中成员变量类所分配的内存也不能被释放,因为子类析构函数没有被调用,其变量的析构函数肯定也没被调用了

20.vector的底层原理++

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

随着元素的加入,它的内部机制会自行扩充空间以容纳新的元素。当vector动态增加时,并不是在原空间之后持续新空间,而是以原大小的两倍另外配置一块较大的空间,然后把原空间拷贝过来,然后才开始在原空间之后构造新空间,并释放原空间。

21.list的底层原理

list的底层是一个双向链表

22.map 、set、multiset、multimap的底层原理

都是红黑树

红黑树的特性:

  • 根节点是黑色的
  • 每个节点到其子孙节点的所有路径上都包含相同数目的黑色节点
  • 红黑相间

23.const与#define比较,const有什么优点?

  1. const常量有数据类型,而宏常量没有数据类型.编译器可以对前者进行类型安全检查
  2. 有些集成化的调试工具可以对const常量进行调试,但不能对宏常量进行调试.

24. main函数执行以前,还会执行什么代码?

全局对象的构造函数会在main函数之前执行

25. 子类与父类构造函数与析构函数调用顺序

定义一个对象时: 先调用基类的构造函数,然后调用派生类的构造函数;
析构一个对象时:先调用派生类的析构对象,然后调用基类的析构函数

26.delete与delete[]区别

delete只会调用一次析构函数,而delete[]会调用每一个成员的析构函数

27. c 和c++ 有什么区别

  • c是面向过程的语言,c++是面向对象的语言
  • c和c++动态管理内存的方法不一样,c是使用malloc/free函数,而c++还有new/delete关键字
  • c++的类是c所没有的,并对c的struct进行了扩展,是其可以像class一样当做类使用
  • c++支持函数重载,c不支持
  • c++有引用,c没有
  • 还有局部变量声明规则不同,多态,输入输出流等

28. 二叉树0,1,2节点

  • 二叉树中只有一个孩子的二叉树度为
  • 节点中有两个孩子的二叉树度为2
  • 没有孩子的二叉树度为0
  • 二叉树节点计算公式 N = n 0 + n 1 + n 2 N = n0+n1+n2 N=n0+n1+n2,度为0的叶子节点比度为2的节点数多一个。 N = 1 ∗ n 1 + 2 ∗ n 2 + 1 N=1*n1+2*n2+1 N=1n1+2n2+1
  • 具有n个节点的完全二叉树的深度为 l o g 2 ( n ) + 1 log2(n) + 1 log2(n)+1

29. 多态的实现原理⭐

  1. 用virtual关键字声明的函数叫做虚函数
  2. 存在虚函数的类都有一个一维的虚函数表叫做虚表。当类中声明虚函数时,编译器会在类中生成一个虚函数表
  3. 类的对象有一个指向虚表开始的虚指针。虚表和类对应,虚表指针和对象对应。
  4. 当存在虚函数时,每个对象都有一个指向虚函数的指针
  5. 子类继承时,会继承虚函数指针和虚函数表,成为自己的虚函数表

30.析构函数可以是虚函数吗?++

析构函数可以成为虚函数
而构造函数不可能成为虚函数,因为只有在构造函数执行结束后,虚函数表指针才能被正确的初始化

当一个类不被当做基类、或者不具有多态性时,令其析构函数为虚函数是多余的,浪费内存。

当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间。所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。

31.为什么调用普通函数比调用虚函数效率高?

因为普通函数是静态联编的,而调用虚函数是动态联编的。
联编的作用:程序调用函数,编译器决定使用哪个可执行代码块。

  • 静态联编:在编译的时候就确定了函数的地址,然后就可以调用了
  • 动态联编:首先需要取到对象的首地址,然后再解引用取到虚函数表的首地址,再加上偏移量才能找到要调的虚函数,然后才能使用。

32. 为什么要用虚函数表(存函数指针的数组)?

  • 实现多态,父类对象的指针指向父类对象调用的是父类的虚函数,指向子类调用的是子类的虚函数
  • 同一个类的多个对象的虚函数表是同一个,所以这样就可以节省空间,一个类自己的虚函数和继承的虚函数还有重写父类的虚函数都会存在自己的虚函数表中。

33.什么是虚函数?++

拥有virtual关键字的函数被称为虚函数
虚函数的作用是实现动态绑定,也就是说在程序运行的时候动态的选择合适的成员函数

要成为虚函数必须满足两点:

  1. 这个函数依赖于对象调用,因为虚函数是存在于虚函数表中,有一个虚函数指针指向这个虚表,所以要调用虚函数,必须通过虚函数指针,而虚函数指针存放在对象中。
  2. 这个函数必须可以取地址,因为虚函数表中存放的是虚函数的函数入口地址,如果函数不能寻址,就不能成为虚函数。

34. 哪些函数不能成为虚函数?

  1. 内联函数:内联函数只是在函数调用点将其展开,它不能产生函数符号,所以不能忘虚表中存放,自然就不能成为虚函数
  2. 静态函数:定义为静态函数的函数,这个函数只与类有关系,它不完全依赖于对象调用,所以不能成为虚函
  3. 构造函数:只有当调用了构造函数,这个对象才能产生。如果把构造函数写成虚函数,这时候我们的对象就没有办法生成了,更别说用对象去调用了,所以构造函数不能成为虚函数。
  4. 友元函数:友元函数不属于类的成员函数,不能继承。对于没有继承特性的函数没有虚函数的说法。
  5. 普通函数:普通函数不属于成员函数,是不能被继承的。普通函数只能被重载,不能被重写。

35.当类存在继承的情况下,我们需要注意什么?

若存在继承关系时,析构函数必须申明为虚函数,这样父类指针指向子类对象,释放基类指针时才会调用子类的析构函数释放资源,否则内存泄漏。

36. 简述C++从代码到可执行二进制文件的过程

C++和C语言类似,一个C++程序从源码到执行文件,有四个过程,预编译、编译、汇编、链接。

  • 预编译:这个过程主要的处理操作如下:
    (1) 将所有的#define删除,并且展开所有的宏定义
    (2) 处理所有的条件预编译指令,如#if、#ifdef
    (3) 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。
    (4) 过滤所有的注释,如//、/**/
    (5) 添加行号和文件名标识。
  • 编译:这个过程主要的处理操作如下:
    (1) 词法分析:将源代码的字符序列分割成一系列的记号。
    (2) 语法分析:对记号进行语法分析,产生语法树。
    (3) 语义分析:判断表达式是否有意义。
    (4) 代码优化:
    (5) 目标代码生成:生成汇编代码。
    (6) 目标代码优化:
  • 汇编:这个过程主要是将汇编代码转变成机器可以执行的指令。
  • 链接:将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。

37.说说volatile和mutable

mutable是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中,甚至结构体变量或者类对象为const,其mutable成员也可以被修改。mutable在类中只能够修饰非静态数据成员。
一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器每次会从内存里重新读取这个变量的值,而不是从寄存器里读取。特别是多线程编程中,变量的值在内存中可能已经被修改,而编译器优化优先从寄存器里读值,读取的并不是最新值。这就是volatile的作用了。

38. 说说volatile的应用

Volatile主要有三个应用场景:
(1)外围设备的特殊功能寄存器。
(2)在中断服务函数中修改全局变量。
(3)在多线程中修改全局变量。

39. C++11新特性

  1. nullptr代替NULL :

C++ 不允许直接将 void * 隐式转换到其他类型,但如果 NULL 被定义为 ((void*)0),那么当编译char *ch = NULL;时,NULL 只好被定义为 0。

而这依然会产生问题,将导致了 C++ 中重载特性会发生混乱,考虑:

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

对于这两个函数来说,如果 NULL 又被定义为了 0 那么 foo(NULL); 这个语句将会去调用 foo(int),从而导致代码违反直观。

为了解决这个问题,C++11 引入了 nullptr 关键字,专门用来区分空指针、0

  1. auto :编译器自己推导类型

  2. 初始化列表: A a {1, 1.1};

  3. 连续尖括号合法

  4. Lambda 表达式

Lambda 表达式的基本语法如下:

[ caputrue ] ( params ) opt -> ret { body; };
  • capture是捕获列表;
  • params是参数表;(选填)
  • opt是函数选项;可以填mutable,exception,attribute(选填)

mutable:说明lambda表达式体内的代码可以修改被捕获的变量,并且可以访问被捕获的对象的non-const方法。
exception:说明lambda表达式是否抛出异常以及何种异常。
attribute:用来声明属性。

  • ret是返回值类型(拖尾返回类型)。(选填)
  • body是函数体。

40. 静态变量和静态函数 ++

关键词static

  • 静态全局变量和全局变量的区别:
    静态全局变量不能被其他文件使用,而全局变量则可以通过在其他文件中使用extern声明来使用
  • 静态局部变量与局部变量的区别:
    静态局部变量存储在全局数据区,仅能被局部调用,不是保留在栈中,函数体调用结束后,静态局部变量仍能保持原值、
  • 静态函数与普通函数区别:
    静态函数不能被其他文件调用,而普通文件可以,好处是可以避免与其他文件中的函数重名
  • 静态数据成员与普通数据成员的区别:
    静态数据成员存储在全局数据区,为所有类的对象共享,既能通过类来调用,也能通过对象来调用。
    普通数据成员为对象所拥有的,初始化在构造函数中初始化,调用只能通过对象调用

静态数据成员初始化方式为:

<数据类型> <类名> : : <静态数据成员> = 值,调用方式为 <类对象名>.<静态数据成员名> 

或者

 <类类型名> : : <静态数据成员名> .
  • 静态成员函数与普通成员函数的区别
    静态成员函数为所有类的对象共享,不属于某一具体对象,因此没有this指针,也就不能访问非静态成员变量以及非静态成员函数。另外静态成员函数定义不需要加“static”关键字
    普通成员函数为某一个具体对象所拥有,可以调用静态的数据成员以及成员变量。

41.STL中各种数据结构操作的时间复杂度比较

在这里插入图片描述
unordered_map :O(1)为平均时间,最坏情况下的时间复杂度为O(n)

42.STL 底层实现

  • vector是动态数组,随着元素的加入,它的内部机制会自行扩充空间以容纳新的元素。当vector动态增加时,并不是在原空间之后持续新空间,而是以原大小的两倍另外配置一块较大的空间,然后把原空间拷贝过来,然后才开始在原空间之后构造新空间,并释放原空间。
  • list 容器底层是一个双向链表。
  • deque是一种双向开口的连续线性空间,元素也是在堆中。
    由一段一段的连续空间构成,它的保存形式如下:[堆1] --> [堆2] -->[堆3] --> …

43.包含纯虚函数的类可以被实例化吗?

c++中包含纯虚函数的类是不允许被实例化的,进一步说,如果继承该类的类不重写这个纯虚函数的话,也是不允许被实例化的。即包含纯虚函是的类派生出来的类都必须重写这个纯虚函数!

为什么要有这个机制呢?

例如动物可以派生出猫、狗等。 猫和狗可以实例化,而动物这个概念是不可以实例化的。

44.为什么unordered_map搜索时间:O(1)为平均时间,最坏情况下的时间复杂度为O(n),而map是O(log(n))?

unorded_map使用的是哈希表,所以正常是O(1),当出现哈希冲突时,最坏情况下会O(n)。
而map对存储的数据进行了排序,使用二分搜索的复杂度是O(log(n))。

45.resize(10,0)

在这里插入图片描述

46.const关键字 ++

  1. const修饰普通类型的变量:
const int  a = 7;

a被定义为一个常量,不能再给a赋值。如果还想改变a,需要加volatile关键字:

volatile const int  a = 7;

或者使用const_cast用于将const变量转为非const

  1. const修饰指针变量:

从右往左读的记忆方式:
注意:
允许把非const对象的地址赋给指向const对象的指针,不允许把一个const对象的地址赋给一个普通的、非const对象的指针

  • const 修饰指针所指的内容,则内容为不可变量
const int *p = 8;

指针所指的内容8不可改变,因为const位于*号的左边

  • const 修饰指针,则指针为不可变量
int* const p = &a;

const位于*号的右边,表示const指针p所指的内存地址不能改变。

  • const修饰指针和指针所指的内容,则指针和指针所指内容都为不可变量
const int * const  p = &a;
  1. const参数传递和函数返回值
  • 值传递的const修饰
void Cpf(const int a)
{
    cout<<a;
    // ++a;  是错误的,a 不能被改变
}
  • 当const参数为指针时,防止指针被意外篡改
void Cpf(int *const a)
{
    cout<<*a<<" ";
    *a = 9;
}
  • 对于自定义类型的参数传递,系统会临时赋值参数,对于临时对象的构造,需要调用构造函数,比较浪费时间。使用const+引用传递就可以避免
class Test
{
public:
    Test(){}
    Test(int _m):_cm(_m){}
    int get_cm()const
    {
       return _cm;
    }
private:
    int _cm;
};
void Cmf(const Test& _tt)
{
    cout<<_tt.get_cm();
}
  1. const修饰函数的返回值
  • const修饰内置类型的返回值:修饰语不休时返回值作用一样
const int Cmf()
{
    return 1;
}
  • const 修饰自定义类型的作为返回值,此时返回的值不能作为左值使用,既不能被赋值,也不能被修改。
  • const 修饰返回的指针或者引用,是否返回一个指向 const 的指针,取决于我们想让用户干什么。
  1. const修饰类成员函数
    目的是为了防止成员函数修改被调用对象的值。如果我们不想修改一个调用对象的值,所有成员函数都应该声明为const成员函数。

47.git分支2有问题吗,需要修改后再合并,应该怎么做?

1->2->3->4 ,只是当前的commit链,此时review发现commit2出现问题,可以git checkout 2 -b test_branch,然后在test_branch进行修改,git commit --amend追加到2 commit上面,然后通过git cherry -pick3和4完成,还有一个rebase的也可以。

  • 创建新分支:git branch branchName

  • 切换到新分支:git checkout branchName
    上面两个命令也可以合成为一个命令:

  • 创建新分支并切换到此分支: git checkout -b branchName

  • 将上次提交错误的信息覆盖 : git commit --amend

  • git cherry-pick可以选择某一个分支中的一个或几个commit(s)来进行操作。例如,假设我们有个稳定版本的分支,叫v2.0,另外还有个开发版本的分支v3.0,我们不能直接把两个分支合并,这样会导致稳定版本混乱,但是又想增加一个v3.0中的功能到v2.0中,这里就可以使用cherry-pick了。

  • 当执行完 git cherry-pick 以后,将会 生成一个新的提交;这个新的提交的哈希值和原来的不同,但标识名 一样

  • git merge 操作合并分支会让两个分支的每一次提交都按照提交时间(并不是push时间)排序,并且会将两个分支的最新一次commit点进行合并成一个新的commit,最终的分支树呈现非整条线性直线的形式

  • git rebase操作实际上是将当前执行rebase分支的所有基于原分支提交点之后的commit打散成一个一个的patch,并重新生成一个新的commit hash值,再次基于原分支目前最新的commit点上进行提交,并不根据两个分支上实际的每次提交的时间点排序

48.cmake实现机制

CMake有三个关键概念:target(目标),generator和command(命令)。在CMake中,这些东西本质都是c++的类。
CMake处理的最底层的东西就是若干个源代码文件,这些源文件被整合成一个或多个target,target一般情况下是一个可执行文件或者是库文件

49.weak指针的作用和实现机制

一般结合强智能指针使用,它指向一个shared_ptr管理的对象,进行该对象的内存管理的是强引用shared_ptr。weak_ptr只提供对管理对象的一个访问手段,设计目的是为了配合shared_ptr,不会引起引用次数的增加或减少。
针对于强智能指针(shared_ptr )相互引用会出现内存无法释放的情况,加入弱智能指针(weak_ptr)可以解决。

50.如何解决哈希冲突?

  • hash冲突:
    对应不同的关键字可能会获得相同的hash地址,即 k e y 1 ≠ k e y 2 key1≠key2 key1=key2,但是 f ( k e y 1 ) = f ( k e y 2 ) f(key1)=f(key2) f(key1)=f(key2)。这种冲突只能尽可能减少,不能完全避免。
  • 处理冲突的方法:
  1. 拉链法:HashSet都是采用拉链法来解决哈希冲突的。将所有关键字为同义词的结点链接在同一个单链表中。
  2. 开放定址法:当冲突发生时,使用某种探查技术再散列表中形成一个探查序列。沿此序列逐个单元的查找,直到找到给定的关键字,或者碰到一个开放的地址(即该地址单元为空)为止。
  3. 再散列法:就是再使用哈希函数去散列一个输入的时候,输出是同一个位置就再次散列,直至不发生冲突(每次冲突都要重新散列,时间代价高)

针对哈希冲突主要思路有两种:

1.闭散列

2.开散列

  • 闭散列:又叫开放地址法或者线性探测法
    当我们要往哈希表中插入一个数据时,通过哈希算法计算key的hashcode,当我们找到该位置时,发现已经有元素了。此时我们就紧跟着找这一位置的下一个位置,看是否存在元素。如果能就插入,不能就探测紧跟着当前位置的下一个位置。
  • 开散列:又叫拉链法
    把发生冲突的关键码存储在散列表主表之外。

51.拷贝构造函数为什么要传引用不能传值

拷贝构造函数的标准写法如下:

class Base
{
public:
  Base(){}
  Base(const Base &b){..}
  //
}
  • 值传递: 对于内置数据类型的传递,直接赋值拷贝给形参(形参表示为函数内的局部变量)
    对于类类型的传递,首先要调用该类的拷贝构造函数来初始化形参
  • 引用传递: 无论是内置数据类型的传递还是类类型,传递引用或指针最终都是传递地址值,不会有拷贝构造函数的调用

如果把拷贝构造函数的参数设置为值传递,那么参数就是本类的一个object,采用值传递,在形参和实参相结合的时候,需要调用本类的构造函数,就构成了一个死循环递归。

52.多态的静态联编

静态联编:编译时期确定了调用函数地址(运算符重载,函数重载)
动态联编:执行时期确定了调用函数的地址

53.设计模式

https://www.cnblogs.com/chengjundu/p/8473564.html

  1. 工厂模式
    在工厂模式中,我们在创建对象不会对客户端暴露创建逻辑,并且是通过使用一个共同的接口来指向新创建的对象。
  2. 策略模式
    策略模式指定义一系列算法,把它们单独封装起来,并且使用它们可以互相替换,使得算法可以独立于使用它的客户端而变化,也是说这些算法所完成的功能类型是一样的,对外接口也是一样的,只是不用的策略会引起环境校测表现出不同的行为
  3. 适配器模式
    适配器模式可以将一个类的接口转换成客户端希望的另一个接口,使得原来由于…

54.内存溢出和内存泄漏

内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory;比如申请了一个integer,但给它存了long才能存下的数,那就是内存溢出。

内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光。

个人用通俗的话来理解就是: 内存溢出,就是说,你向系统申请了装10个橘子的篮子(内存)并拿到了,但你却用它来装10个苹果,从而超出其最大能够容纳的范围,于是产生溢出; 内存泄漏,就是说系统的篮子(内存)是有限的,而你申请了一个篮子,拿到之后没有归还(忘记还了或是丢了),于是造成一次内存泄漏。在你需要用篮子的时候,又去申请,如此反复,最终系统的篮子无法满足你的需求,最终会由内存泄漏造成内存溢出。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐