相关文章推荐
发财的山羊  ·  C++ ...·  3 周前    · 
想发财的脸盆  ·  团宠公主三岁半 ...·  1 年前    · 
C++面试总结

C++面试总结

1 年前 · 来自专栏 程序员面经

c++基础知识

1. C++程序中变量在内存中分配情况

程序 代码区

常量区 存放常量。程序结束时由 OS回收

全局区(静态区) 存放 全局变量 静态变量,局部常量 。初始化的全局变量和静态变量在一块区域,未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 程序结束时由 OS回收

堆区 存放的变量(用new,malloc,calloc,realloc等分配内存函数得到的变量)由 程序员分配释放

存放的变量(局部变量、函数参数等)由 编译器自动分配 释放。

2. 堆和栈有什么区别

堆是由 new和malloc 开辟的⼀块内存,由 程序员⼿动管理

栈是 编译器⾃动管理 的内存,存放函数的 参数和局部变量 。连续的内存空间,在函数调⽤的时候,⾸先⼊栈的主函数的下⼀条可执⾏指令的地址,然后是函数的各个参数。

堆空间因为会有频繁的分配释放操作,会产⽣内存碎⽚。 不连续的空间,实际上系统中有⼀个空闲链表,当有程序申请的时候,系统遍历空闲链表找到第⼀个⼤于等于申请⼤⼩的空间分配给程序,⼀般在分配程序的时候,也会空间头部写⼊内存⼤⼩,⽅便 delete 回收空间⼤⼩。当然如果有剩余的,也会将剩余的插⼊到空闲链表中,这也是产⽣内存碎⽚的原因。

堆的⽣⻓空间向上,地址越来越⼤;

栈的⽣⻓空间向下,地址越来越⼩。

3. 堆快⼀些还是栈快⼀些

栈快⼀些。

操作系统在底层会对栈提供⽀持 ,会分配 专⻔的寄存器存放栈的地 址,栈的 ⼊栈出栈 操作也⼗分简单,并且有 专⻔的指令执⾏ ,所以栈的效率⽐价快也⽐较⾼。

⽽堆的操作是由 C/C++函数库提供的 ,在分配堆内存的时候,需要⼀定的 算法寻找 合适的内存⼤⼩,并且获取堆的内容需要 两次访问, 第⼀次访问指针,第⼆次 根据指针保存的地址访问内存 ,因此堆⽐较慢。

4. new和delete是如何实现的,new和malloc的异同

在new⼀个对象的时候,分两步⾸先会调⽤malloc为对象分配内存空间,然后调⽤对象的构造函数。

delete也分两步,首先会调⽤ 对象的析构函数 ,然后调⽤ free 回收内存。

new 和 malloc都会分配空间,但是new还会根据调⽤对象的构造函数进⾏初始化,malloc需要给定空间⼤⼩,⽽new只需要对象名。

总结(你会发现new 和delete 比malloc 和free各多一步,调用构造函数和析构函数的过程)

5. 既然有了malloc/free,为什么还要new/delete

malloc/free 是c/c++中的 标准库函数 new/delete 是c++中的 运算符 。它们都⽤于申请动态内存和释放内存。

对于⾮内部数据对象(如 类对象 ),只⽤malloc/free⽆法满⾜ 动态对象 的要求。这是因为对象在创建的同时需要⾃动执⾏构造函数,对象在消亡之前要⾃动执⾏析构函数,⽽由于 malloc/free是库函数⽽不是运算符 ,不在 编译器的控制权限之内 ,也就不饿能⾃动执⾏构造函数和析构函数。因此,不能将执⾏构造函数和析构函数的任务强加给malloc/free。所以,在 c++中需要⼀个能完成动态内存分配和初始化⼯作的运算符new,以及⼀个能完成清理和释放内存⼯作的运算符delete

6. C和C++的区别

C⾯向过程 c++⾯向对象

c++有 封装,继承和多态 的特性。封装隐藏了实现细节,使得代码模块化。继承通过⼦类继承⽗类的⽅法和属性,实现了代码重⽤。多态则是“⼀个接⼝,多个实现”,通过⼦类重写⽗类的虚函数,实现接⼝重⽤。

C和C++内存管理的⽅法不⼀样,C使⽤ malloc/free ,C++除此之外还⽤ new/delete。

C++中还有函数重载和引⽤等概念,C中没有。

C++是面向对象的语言,而C是面向过程的语言; C++引入new/delete运算符,取代了C中的malloc/free库函数; C++引入引用的概念,而C中没有; C++引入类的概念,而C中没有; C++引入函数重载的特性,而C中没有

多态介绍:

C/C++中的多态理解_Lynnllh的博客-CSDN博客_c 多态

7. c++和python区别

1. python是⼀种脚本语⾔,是 解释执⾏ 的,⽽C++是编译语⾔,是需要 编译后在特定平台运⾏的 。python可以 很⽅便的跨平台 ,但是效率没有C++⾼。

2. python使⽤ 缩进来区分不同的代码块 ,C++使⽤ 花括号来区分

3. C++中需要事 先定义变量的类型 ,⽽python不需要,python的基本数据类型只有数字,布尔值,字符串,列表,元组等等

4. python的库函数⽐C++的多,调⽤起来很⽅便

参考: 编译型语言和解释型语言的区别

8. Struct和class的区别

struct的成员的访问权限默认是 public ,⽽class的成员 默认是private

struct的继承默认是 publilc继承 ,⽽class的默认继承是private继承;

class可以在作为模板 ,⽽ struct不可以

9. define和const的联系与区别

联系:他们都是定义常量的⼀种⽅法。

区别:define 定义的变量没有类型 ,只是进⾏ 简单的替换 ,空间⼤;

const定义的常量是有类型的,存放在 静态存储区 ,只有 ⼀个拷⻉ ,占⽤的内存空间⼩。

编译器处理方面不同:define定义的常量实在 预处理阶段进⾏替换 ,⽽ const在编译阶段确定它的值

类型检查不同:define 不会进⾏安全类型检查 ,⽽const会进⾏ 类型安全检查 ,安全性更⾼;

const可以定义函数 define不可以

10. 在c++中const的⽤法(定义、⽤途)

const修饰变量: 变量的值不能改变

const修饰指针:

int const *p1 = &b; //const 在前,定义为常量指针
int *const p2 = &c; // *在前,定义为指针常量
指针和 const 谁在前先读谁 ;
*象征着地址,const象征着内容;
谁在前面谁就不允许改变。

常量指针是指指向常量的指针 ,顾名思义,就是 指针指向的是常量 ,即,它不能指向变量,它指向的内容不能被改变,不能通过指针来修改它指向的内容,但是指针自身不是常量,它自身的值可以改变,从而指向另一个常量。

指针常量是指指针本身是常量 它指向的地址是不可改变的 ,但地址里的内容可以通过指针改变。它指向的地址将伴其一生,直到生命周期结束。有一点需要注意的是,指针常量在定义时必须同时赋初值。

const修饰类的成员变量 :const 成员变量,只在某个对象⽣命周期内是常量,⽽对于整个类⽽⾔是可以改变的。因为类可以创建多个对象,不同的对象其 const 数据成员值可以不同。所以不能在类的声明中初始化 const 数据成员,因为类的对象在没有创建时候,编译器不知道 const数据成员的值是什么。 const 数据成员的初始化只能在类的构造函数的初始化列表中进⾏。 const 成员函数:const 成员函数的主要⽬的是防⽌成员函数修改对象的内容。要注意,const关键字和 static 关键字对于成员函数来说是不能同时使⽤的,因为 static 关键字修饰静态成员函数不含有 this 指针,即不能实例化,const 成员函数⼜必须具体到某⼀个函数。

const修饰类的成员函数:常量对象可以调⽤类中的 const 成员函数,但不能调⽤⾮ const 成员函数; (原因:对象调⽤成员函数时,在形参列表的最前⾯加⼀个形参 this,但这是隐式的。this 指针是默认指向调⽤函数的当前对象的,所以,很⾃然,this 是⼀个常量指针 test * const,因为不可以修改this 指针代表的地址。但当成员函数的参数列表(即⼩括号)后加了 const 关键字(void print() const;),此成员函数为常量成员函数,此时它的隐式this形参为 const test * const,即不可以通过 this 指针来改变指向对象的值。

11. C++中static的⽤法和意义

修饰局部变量 :static⽤来修饰局部变量的时候,它就改变了局部变量的存储位置,从原来的 栈中 存放改为 静态存储区 。本来生命周期在语句块执行结束时变结束了,但是statci修饰以后,变量在静态存储区,生命周期一直延续到整个程序结束。但是注意作用域并没有边,还是在语句块内。

修饰全局变量 :当static⽤来修饰全局变量的时候, 没有改变它的存放位置,还是在静态存储区中 。但它就改变了全局变量的作⽤域,由原来的整个工程编程本文件可见,注意:也可以在同⼀个⼯程中其它源⽂件被访问(添加 extern进⾏声明即可)。

修饰函数 :被static修饰过的函数就是 静态函数 ,静态函数只能在 本⽂件中使⽤ 不能被其他⽂件调⽤ ,也不会和其他⽂件中的同名函数冲突。情况和修饰全局变量类似。

修饰类 :在类中,被static修饰的成员变量是类静态成员,这个静态成员会被类的多个对象共⽤。 被static修饰的成员函数也属于静态成员 不是属于某个对象的 ,访问这个静态函数不需要引⽤对象名,⽽是 通过引⽤类名 来访问。

扩展:

函数体内 static 变ᰁ的作⽤范围为该函数体,不同于 auto 变ᰁ,该变ᰁ的内存只被分配⼀次,因此其值在下次调⽤时仍维持上次的值;

在模块内的 static 全局变ᰁ可以被模块内所⽤函数访问,但不能被模块外其它函数访问;

在模块内的 static 函数只可被这⼀模块内的其它函数调⽤,这个函数的使⽤范围被限制在声明它的模块内;

在类中的 static 成员变ᰁ属于整个类所拥有,对类的所有对象只有⼀份拷⻉;

在类中的 static 成员函数属于整个类所拥有,这个函数不接收 this 指针,因⽽只能访问类的 static 成员变ᰁ。

static 类对象必须要在类外进⾏初始化,static 修饰的变ᰁ先于对象存在,所以 static 修饰的变ᰁ要在类外初始化;

由于 static 修饰的类成员属于类,不属于对象,因此 static 类成员函数是没有 this 指针,this 指针是指向本对象的指针,正因为没有 this 指针,所以 static 类成员函数不能访问⾮static 的类成员,只能访问 static修饰的类成员;

static 成员函数不能被 virtual 修饰,static 成员不属于任何对象或实例,所以加上 virtual没有任何实际意义;静态成员函数没有 this 指针,虚函数的实现是为每⼀个对象分配⼀个vptr 指针,⽽ vptr 是通过 this 指针调⽤的,所以不能为 virtual;虚函数的调⽤关系,this->vptr->ctable->virtual function。

12. 定义和声明的区别

声明是告诉编译器变量的类型和名字, 不会为变量分配空间

定义就是对这个变量和函数进⾏ 内存分配和初始化 。需要分配空间, 同⼀个变量可以被声明多次 ,但是 只能被定义⼀次

13. typdef和define区别

(#define是预处理命令,在 预处理 是执⾏简单的替换, 不做类型检查

typedef是在 编译时处 理的(和之前的const一样,在编译时确定变量的值), 它是在⾃⼰的作⽤域内给已经存在的类型⼀个别名

14. 被free回收的内存是⽴即返还给操作系统吗?为什么

不是的,被free回收的内存会⾸先被 ptmalloc使⽤双链表保存起 来,当 ⽤户下⼀次申请内存的时候,会尝试从这些内存中寻找合适的返回 。这样就避免了频繁的系统调⽤,占⽤过多的系统资源。同时ptmalloc也会尝试对 ⼩块内存进⾏合并,避免过多的内存碎⽚

15. STL中的sort()算法是⽤什么实现的,stable_sort()呢

STL的sort算法, 数据量大 时采用 QuickSort快排算法 ,分段归并排序。 一旦分段后的数据量小于某个门槛 ,为避免QuickSort快排的递归调用带来过大的额外负荷,就改用 Insertion Sort插入排序 。如果递归层次过深,还会改用 HeapSort堆排序 。stable_sort()是归并排序。

16. 指针和引用的区别

(1)非空区别。在任何情况下都不能使用指向空的引用。一个引用必须总是指向某些对象。

(2)合法性区别。在使用引用之前不需要测试它的合法性。相反,指针则总是被测试,防止其为空。

(3)可修改区别。指针可以被重新赋值,但是引用自然总是指向在初始化时被指定的对象,以后不能改变,但是指定的对象其内容可以改变。

(4)应用区别。在以下情况使用指针:可以不指向任何对象,可以指向不同对象。如果总是指向同一个对象且不会改变指向,则使用引用。

17. 在函数传递参数时,什么时候⽤指针,什么时候⽤引⽤

对栈空间⼤⼩⽐较敏感(如递归)的时候使⽤引⽤。使⽤引⽤不需要创建临时变量,开销更⼩;

类对象作为参数传递时使⽤引⽤ ,这是C++类对象传递的标准⽅式。

18. 为什么C++没有实现垃圾回收

实现⼀个垃圾回收器会带来额外的空间和时间开销。你需要开辟⼀定的空间保存指针的引⽤计数和对他们进⾏标记mark。然后需要单独开辟⼀个线程在空闲的时候进⾏free操作。

垃圾回收会使得C++不适合进⾏很多底层的操作。

下面是c++作者本人所说:

19. 有⼀个类A,⾥⾯有个类B类型的b,还有⼀个B类型的*b,什么情况下要⽤到前者,什么情况下⽤后者?

⼀个 具体的类 和⼀个 类的指针 ,主要差别就是占据的内存⼤⼩和读写速度。类占据的内存⼤,但是 读写速度快 类指针内存⼩ ,但是读写需要 解引⽤ 。所以可知,以搜索(需要读)为主的场景中,应当使⽤类。以插⼊删除(指定位置插入不需要读)为主的场景中,应当使⽤类指针。

20 a与&a的区别

int a[3] = {1,2,3};

a表示数组首元素的地址,*(a+1)表示数组中下个元素的地址。

&a+1代表整个数组的地址,+1应该加上sizeof(a)的长度,所指向a[3]位置处。

21 #ifdef、#else、#endif、#ifndef的作用

利用#ifdef、#endif将某程序功能模块包括进去,以向特定用户提供该功能。在不需要时用户可轻易将其屏蔽。

虽然不用条件编译命令而直接用if语句也能达到要求,但那样做目标程序长(因为所有语句都编译),运行时间长(因为在程序运行时间对if语句进行测试)。而采用条件编译,可以减少被编译的语句,从而减少目标程序的长度,减少运行时间。

22 sizeof 和strlen 的区别

sizeof是一个操作符,strlen是库函数。

sizeof的参数可以是数据的类型,也可以是变量,而strlen只能以结尾为‘\0’的字符串作参数。

编译器在编译时就计算出了sizeof的结果,而strlen函数必须在运行时才能计算出来。

sizeof计算的是数据类型占内存的大小,而strlen计算的是字符串实际的长度。

23 strcpy、sprintf 与memcpy 的区别

操作对象不同,strcpy 的两个操作对象均为字符串,sprintf 的操作源对象可以是多种数据类型, 目的操作对象是字符串,memcpy 的两个对象就是两个任意可操作的内存地址,并不限于何种数据类型。

执行效率不同,memcpy 最高,strcpy 次之,sprintf 的效率最低。

实现功能不同,strcpy 主要实现字符串变量间的拷贝,sprintf 主要实现其他数据类型格式到字 符串的转化,memcpy 主要是内存块间的拷贝。

24 C++中指针传递和引用传递

指针参数传递本质上是值传递,它所传递的是⼀个地址值。值传递过程中,被调函数的形式参数作为被调函数的局部变ᰁ处理,会在栈中开辟内存空间以存放由主调函数传递进来的实参值,从⽽形成了实参的⼀个副本(替身)。值传递的特点是,被调函数对形式参数的任何操作都是作为局部变ᰁ进⾏的,不会影响主调函数的实参变ᰁ的值(形参指针变了,实参指针不会变)。

值传递:

void f( int  p){
    printf("\n%x",&p);
    printf("\n%x",p);
    p=0xff;
int main()
    int a=0x10;
    printf("\n%x",&a);
    printf("\n%x\n",a);
    f(a);
    printf("\n%x\n",a);
	return 0;
e4a3a418
e4a3a3ec
10


引⽤参数传递过程中,被调函数的形式参数也作为局部变量在栈中开辟了内存空间,但是这时存放的是由主调函数放进来的实参变量的地址。被调函数对形参(本体)的任何操作都被处理成间接寻址,即通过栈中存放的地址访问主调函数中的实参变量(根据别名找到主调函数中的本体)。因此,被调函数对形参的任何操作都会影响主调函数中的实参变量。引⽤传递和指针传递是不同的,虽然他们都是在被调函数栈空间上的⼀个局部变量,但是任何

对于引⽤参数的处理都会通过⼀个间接寻址的⽅式操作到主调函数中的相关变量。⽽对于指针传递的参数,如果改变被调函数中的指针地址,它将应⽤不到主调函数的相关变量。如果想通过指针参数传递来改变主调函数中的相关变量(地址),那就得使⽤指向指针的指针或者指针引⽤。

void func(int & p){
    printf("\n%x",&p);
    printf("\n%x",p);
    p=20;
int main()
    int a=10;
    printf("\n%x",&a);
    printf("\n%x\n",a);
    func(a);
    printf("\n%x\n",a);
	return 0;
e6e59418
e6e59418
14

主调函数调用func把实参传给p时,实际上是给实参起了个别名p,所以在函数中对p的操作就是对主调函数中的对应实参的操作,将会使实参发生永久性改变。

可以这样看,形参传递后,int& p = a; p就是a的别名,改变p的值就是改变a的值。

下面再看一下指针的指针

void func(int * p){
    printf("\n%x",&p);
	printf("\n%x",p);
	printf("\n%x\n",*p);
	*p=20;
int main()
    int a=10;
    printf("\n%x",&a);
    printf("\n%x\n",a);
    func(&a);
    printf("\n%x\n",a);
	return 0;
ee86d418
ee86d3e8
ee86d418
14

通过指针传递的案例我们可以看到,调用f(&a)是将a的地址0x12ff44传递给p,则*p就指向了a的内容,改变*p后,a的内容自然就改变了,示意图如下:

可以这样看,形参传递后 int * p = &a 就是p指向变量a的地址,*p就是变量a地址里面的值。

下面看一下指针的引用传递

void func(int * &p){
    printf("\n%x",&p);
    printf("\n%x",p);
    printf("\n%x\n",*p);
    *p=20;
int main()
 int a=10;
    printf("\n%x",&a);
    printf("\n%x\n",a);
 int  *b = &a;
    printf("\n%x",&b);
    printf("\n%x\n",b);
    printf("\n%x\n",*b);
    func(b);
    printf("\n%x\n",a);
 return 0;
e642b418
e642b410
e642b418
e642b410
e642b418
14

为了使用指针的引用传递我们要新建一个指针b,然后将b的引用传递给p,其实p就是b的一个拷贝,而且p和b的地址是一样的。*p=*b都指向a,所以改变*p的内容也就改变a的内容。示意图如下:

如果不使用指针的引用传递呢

void func(int *p){
    printf("\n%x",&p);
	printf("\n%x",p);
	printf("\n%x\n",*p);
	*p=20;
int main()
    int a=10;
    printf("\n%x",&a);
    printf("\n%x\n",a);
    int  *b = &a;
    printf("\n%x",&b);
    printf("\n%x\n",b);
    printf("\n%x\n",*b);
    func(b);
    printf("\n%x\n",a);
	return 0;
e78eb418
e78eb410
e78eb418
e78eb3d8
e78eb418
14

可以看出p和b的地址是不一样的,虽然他们都指向a

总结一下:

1.  
int a = 10;
int *b = &a; 
定义了一个指针b指向a,b的值是一个地址,这个地址里面存储的是变量a的值
int a = 10;
int *b = &a;
int *p = b;
定义了一个指针b指向a,b的值是一个地址,这个地址里面存储的是变量a的值
定一个一个指向p,&p和&b即b的地址和p的地址是不一样的,但是p和b的值(a的地址)是一样的,
说明两者都指向a,
int a = 10;
int *b = &a;
int *&p = a;
定义了一个指针b指向a,b的值是一个地址,这个地址里面存储的是变量a的值
定一个一个指向p,&p和&b即b的地址和p的地址是一样的,此时int *&b = a可以看作int *(&p) = a
即p是a的一个引用,p和b的值(a的地址)是一样的,说明两者都指向a,
     

简单说⼀下函数指针

从定义和⽤途两⽅⾯来说⼀下⾃⼰的理解:

⾸先是定义:函数指针是指向函数的指针变ᰁ。函数指针本身⾸先是⼀个指针变量,该指针变量指向⼀个具体的函数。这正如⽤指针变量可指向整型变量、字符型、数组⼀样,这⾥是指向

函数。在编译时,每⼀个函数都有⼀个⼊⼝地址,该⼊⼝地址就是函数指针所指向的地址。有了指向函数的指针变ᰁ后,可⽤该指针变ᰁ调⽤函数,就如同⽤指针变ᰁ可引⽤其他类型变ᰁ⼀样,在这些概念上是⼤体⼀致的。

其次是⽤途:调⽤函数和做函数的参数,⽐如回调函数。

char * fun(char * p) {…} // 函数fun
char * (*pf)(char * p); // 函数指针pf
pf = fun; // 函数指针pf指向函数fun
pf(p); // 通过函数指针pf调⽤函数fun



STL

1. C++的STL介绍

C++ STL从⼴义来讲包括了三类: 算法 容器 迭代器

算法包括排序 ,复制等常⽤算法,以及不同容器特定的算法。

容器 就是数据的存放形式,包括序列式容器和关联式容器,序列式容器就是list, vector 等,关联式容器就是set,map等。

迭代器就是在不暴露容器内部结构的情况下对容器的遍历(这就是迭代器的好处)

迭代器和指针的区别

迭代器不是指针,是类模板, 表现的像指针。他只是模拟了指针的⼀些功能,通过重载了指针的⼀些操作符,->、*、++、--等。迭代器封装了指针,是⼀个“可遍历STL( StandardTemplate Library)容器内全部或部分元素”的对象, 本质是封装了原⽣指针,是指针概念的⼀种提升(lift),提供了⽐指针更⾼级的⾏为,相当于⼀种智能指针,他可以根据不同类型的数据结构来实现不同的++,--等操作。

迭代器返回的是对象引⽤⽽不是对象的值,所以cout只能输出迭代器使⽤*取值后的值⽽不能直接输出其⾃身。(之前刷题的时候cout就无法输出迭代器自身)

2. STL源码中的hash表的实现

STL中的hash表就unordered_map。使⽤的是哈希进⾏实现(注意与map的区别)。unordered_map的底层实现是hashtable,采⽤ 开链法 (数组里面放链表)(也就是⽤桶)来解决 哈希冲突

HashMap本质是一个一定长度的数组,数组中存放的是链表

当HashMap中的元素越来越多的时候,hash冲突的几率也就越来越高,所以为了提高查询的效率,就要对HashMap的数组进行扩容。 HashMap中扩容是调用resize()方法。

参考: HashMap实现原理和扩容机制_渐暖吧的博客-CSDN博客

解决哈希冲突的⽅式

1. 开放地址⽅法:

当发⽣地址冲突时,按照某种⽅法继续探测哈希表中的其他存储单元,直到找到空位置为⽌。

(1) 线性探测

按顺序决定值时,如果某数据的值已经存在,则在原来值的基础上往后加⼀个单位,直⾄不发⽣哈希冲突。

(2) 再平⽅探测

按顺序决定值时,如果某数据的值已经存在,则在原来值的基础上先加1的平⽅个单位,若仍然存在则减1的平⽅个单位。随之是2的平⽅,3的平⽅等等。直⾄不发⽣哈希冲突。

(3)伪 随机探测

按顺序决定值时,如果某数据已经存在,通过随机函数随机⽣成⼀个数,在原来值的基础加上随机数,直⾄不发⽣哈希冲突。

2. 链式地址法(HashMap的哈希冲突解决⽅法)

对于相同的值,使⽤ 链表进⾏连接。使⽤数组存储每⼀个链表

优点:

(1) 拉链法处理冲突简单,且⽆堆积现象 ,即⾮同义词决不会发⽣冲突,因此平均查找⻓度较短;

(2)由于拉链法中各链表上的结点空间是 动态申请 的,故它更适合于造表前 ⽆法确定表⻓ 的情况;

(3)开放定址法为减少冲突,要求装填因⼦α较⼩,故当结点规模较⼤时会浪费很多空间。⽽拉链法中可取α≥1,且结点较⼤时,拉链法中增加的指针域可忽略不计,因此节省空间;

(4)在⽤拉链法构造的散列表中, 删除结点的操作易于实现 。只要简单地删去链表上相应的结点即可。

缺点:

指针占⽤较⼤空间时,会造成空间浪费 ,若空间⽤于增⼤散列表规模进⽽提⾼开放地址法的效率。

3. 再哈希法 :当发⽣哈希冲突时使⽤另⼀个哈希函数计算地址值,直到冲突不再发⽣。这种⽅法不易产⽣聚集,但是增加计算时间,同时需要准备许多哈希函数。

4. 建⽴公共溢出区 :采⽤⼀个溢出表存储产⽣冲突的关键字。如果公共溢出区还产⽣冲突,再采⽤处理冲突⽅法处理。

3. STL中unordered_map和map的区别

unordered_map是使⽤ 哈希实现 ,占⽤内存⽐较多 查询速度⽐较快 ,是常数时间复杂度O(1)。它 内部是⽆序 的。

map底层是采⽤红⿊树实现的,插⼊删除查询时间复杂度都是O(log(n)),它的内部是有序的。

4. vector内存增长

vector所有的内存相关问题都可以归结于它的内存增长策略。vector有一个特点就是:内存空间只会增长不会减少。vector有两个函数,一个是 capacity() ,返回 对象缓冲区 (vector维护的内存空间) 实际申请的空间大小 ,另一个 size() ,返回 当前对象缓冲区存储数据的个数 。对于vector来说,capacity是永远大于等于size的,档capacity和size相等时,vector就会扩容,capacity变大。

比如说vector最常用的push_back操作,它的整个过程是怎么一个机制呢?这个问题经常在面试中出现。

这个问题其实很简单,在调用push_back时,若当前容量已经不能够放入新的元素(capacity=size),那么vector会重新申请一块内存,把之前的内存里的元素拷贝到新的内存当中,然后把push_back的元素拷贝到新的内存中,最后要析构原有的vector并释放原有的内存。所以说这个过程的效率是极低的,为了避免频繁的分配内存, C++每次申请内存都会成倍的增长 ,例如之前是4,那么重新申请后就是8,以此类推。当然不一定是成倍增长,比如在我的编译器环境下实测是0.5倍增长,之前是4,重新申请后就是6。

5. vector使⽤的注意点及其原因,频繁对vector调⽤push_back()对性能的影响和原因

如果需要频繁插⼊,最好先指定vector的⼤⼩,因为vector在容器⼤⼩不够⽤的时候会重新申请⼀块⼤⼩为原容器两倍的空间, 并将原容器的元素拷⻉到新容器中,并释放原空间,这个过程是 ⼗分耗时和耗内存 的。频繁调⽤push_back()会使得程序花费很多时间在vector扩容上,会变得很慢。这种情况可以考虑使⽤list。

6. C++中vector和list的区别

vector和数组类似,拥有⼀段连续的内存空间 。vector申请的是⼀段连续的内存,当插⼊新的元素内存不够时,通常以2倍重新申请更⼤的⼀块内存,将原来的元素拷⻉过去,释放旧空间。因为内存空间是连续的,所以在进⾏插⼊和删除操作时,会造成内存块的拷⻉,时间复杂度为o(n)。

list是由双向链表 实现的,因此内存空间是不连续的。只能通过指针访问数据,所以list的 随机存取⾮常没有效率 ,时间复杂度为o(n); 但由于链表的特点,能 ⾼效地进⾏插⼊和删 除。

vector拥有⼀段连续的内存空间,能很好的⽀持随机存取,因此vector::iterator⽀持“+”,“+=”,“<”等操作符。

list的内存空间可以是不连续,它不⽀持随机访问,因此list::iterator则不⽀持“+”、“+=”、“<”等

vector::iterator和list::iterator都重载了“++”运算符。

总之,如果需要⾼效的随机存取,⽽不在乎插⼊和删除的效率,使⽤vector;如果需要⼤量的插⼊和删除,⽽不关⼼随机存取,则应使⽤list。

7. vector会迭代器失效吗?什么情况下会迭代器失效?

对于 序列容器 vector,deque 来说,使⽤ erase(itertor) 后,后边的每个元素的迭代器都会失效,但是后边每个元素都会往前移动⼀个位置,但是 erase 会返回下⼀个有效的迭代器;

对于 关联容器 map set 来说,使⽤了 erase(iterator) 后,当前元素的迭代器失效,但是其结构是红⿊树,删除当前元素的,不会影响到下⼀个元素的迭代器,所以在调⽤ erase 之前,记录下⼀个元素的迭代器即可。

对于 list 来说,它使⽤了不连续分配的内存,并且它的 erase ⽅法也会返回下⼀个有效的iterator,因此上⾯两种正确的⽅法都可以使⽤。

8. string的底层实现

string继承⾃ basic_string , 其实是对char进⾏了封装 ,封装的 string 包含了 char 数组,容量,⻓度等等属性。 string可以进⾏动态扩展,在每次扩展的时候另外申请⼀块原空间⼤⼩两倍。

9. set,map和vector的插⼊复杂度

map, set , multimap, and multiset

上述四种容器采⽤红⿊树实现,红⿊树是平衡⼆叉树的⼀种。不同操作的

时间复杂度近似为:

插⼊: O(logN)

查看:O(logN)

删除:O(logN)

hashmap, unordered_map, hash_set, hash_multimap, and hash_multiset

上述四种容器采⽤哈希表实现,不同操作的时间复杂度为:

插⼊:O(1),最坏情况O(N)。

查看:O(1),最坏情况O(N)。

删除:O(1),最坏情况O(N)。

vector的复杂度

查看:O(1)

插⼊:O(N)

删除:O(N)

list复杂度(链表实现)

查看:O(N)

插⼊:O(1)

删除:O(1)

set,map的插⼊复杂度就是红⿊树的插⼊复杂度,是log(N)。

unordered_set,unordered_map的插⼊复杂度是常数,最坏是O(N).

vector的插⼊复杂度是O(N),最坏的情况下(从头插⼊)就要对所有其他元素进⾏移动,或者扩容重新拷⻉。

10. set、map特性与区别

set:set是⼀种关联式容器,特性如下:

1. set低层以红⿊树为低层容器;(底层实现:红⿊树)

2. 所得元素的只有key没有value,value就是key;

3. 不允许出现键值重复

4. 所有元素都会被⾃动排序

5. 不能通过迭代器来改变set的值,因为set的值就是键

map:map是⼀种关联式容器,特性如下:

1. map以红⿊树作为底层容器(底层实现:红⿊树)

2. 所有元素都是键key+值value存在

3. 不允许键key重复

4. 所有元素是通 过键进⾏⾃动排序

5. map的键是不能修改的,但是其键对应的值是可以修改的

参考:

带你深入理解STL之Set和Map_ZeeCoder -CSDN博客

11. map、set为什么要⽤红⿊树实现

红⿊树是⼀种⼆叉查找树,但在每个节点上增加⼀个存储为⽤于表示节点的颜⾊,可以是红或者⿊。通过对任何⼀条从根到叶⼦节点的路径上各个节点的着⾊⽅式的限制, 红⿊树确保没有⼀条路径会⽐其他路径⻓出两倍 ,因此,红⿊树是⼀种弱平衡树, 但⼜相对与要求严格的AVL树来说,他的旋转次数较少 ,所以对于 搜索,插⼊,删除 操作⽐较多的情况下,通常使⽤红⿊树。

回答⼀下 STL resize reserve 的区别

resize():改变当前容器内含有元素的数ᰁ(size()),eg: vectorv; v.resize(len);v的size变为len,如果原来v的size⼩于len,那么容器新增(len-size)个元素,元素的值为默认为0.当v.push_back(3);之后,则是3是放在了v的末尾,即下标为len,此时容器是size为len+1;如果大于len,则截断。

reserve():改变当前容器的最⼤容量(capacity),它不会⽣成元素,只是确定这个容器允许放⼊多少对象,如果reserve(len)的值⼤于当前的capacity(),那么会重新分配⼀块能存len个对象的空间,然后把之前v.size()个对象通过 copy construtor 复制过来,销毁之前的内存;


c++特性

1. c++的重载和重写和隐藏

重载(overload)是指函数名相同,可以是参数类型,个数,顺序的不同。它们的返回值可以不同,但返回值不可以作为区分不同重载函数的标志。

重写(overwide)是指函数名相同,参数列表相同,只有⽅法体不相同的实现⽅法。⼀般⽤于⼦类继承⽗类时对⽗类虚函数的重写。⼦类的同名⽅法屏蔽了⽗类⽅法的现象称为隐藏。因为调用这个函数的时候一般是从子类到父类逐级寻找。如果不是虚函数,子类会把父类同名函数屏蔽。

隐藏(重定义):是指派生类的函数屏蔽了与其同名的基类函数,注意只要同名函数并且基类没有virtual修饰,不管参数列表是否相同,基类函数都会被隐藏。注意重载是在同一个类中的,隐藏是子类对父类。

2. ⾯向对象的三⼤特性

⾯向对象的三⼤特性是:封装,继承和多态。

封装隐藏了类的实现细节和成员数据,实现了代码模块化 ,如类⾥⾯的private和public;

继承使得⼦类可以复⽤⽗类的成员和⽅法,实现了代码重⽤

多态则是“⼀个接⼝,多个实现”,通过⽗类调⽤⼦类的成员,实现了接⼝重⽤,如⽗类的指针指向⼦类的对象。

3. 多态的实现

C++ 多态包括编译时多态和运⾏时多态 ,编译 时多态(静态多态)体现在 函数重载 和模板上, 运⾏时多态(动态多态)体现在虚函数上,就是子类对父类的虚函数进行重写

虚函数:在 基类 的函数前加 上virtual 关键字,在 派⽣类中重写该函数 ,运⾏时将会根据对象的 实际类型 来调⽤ 相应的函数 如果对象类型是派⽣类,就调⽤派⽣类的函数;如果对象类型是基类,就调⽤基类的函数

4. 函数重载的时候怎么解决同名冲突

对于C++,同名函数会根据 参数类型和数量的不同 编译成不同的函数名 ,这样在 链接阶段就可以正确的区分 ,从⽽实现重载。

5. C++虚函数相关(虚函数表,虚函数指针),虚函数的实现原理

参考:

C++虚函数表剖析

c++虚函数详解(你肯定懂了)_lyztyycode的博客-CSDN博客_虚函数

C++虚函数表剖析_Leo的博客-CSDN博客_c++虚函数表

C++的虚函数是实现 多态 的机制。它是通过 虚函数表 实现的, 虚函数表是每个类中存放虚函数地址的指针数组 ,类的实例在调⽤函数时会在虚函数表中寻找函数地址进⾏调⽤, 如果⼦类覆盖了⽗类的函数,则⼦类的虚函数表会指向⼦类实现的函数地址 ,否则指向⽗类的函数地址。 ⼀个类的所有实例都共享同⼀张虚函数表

虚表指针放在类的开头。通过对虚表指针的解引⽤找到虚表。如果多重继承和多继承的话,⼦类的虚函数表⻓什么样⼦?多重继承的情况下越是祖先的⽗类的虚函数更靠前,多继承的情况下越是靠近⼦类名称的类的虚函数在虚函数表中更靠前。

虚函数表是⼀个 指针数组 其元素是虚函数的指针 每个元素对应⼀个虚函数的函数指针 。需要指出的是,普通的函数即⾮虚函数,其调⽤并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针。虚表内的条⽬,即虚函数指针的赋值发⽣在编译器的 编译阶段 ,也就是说在代码的编译阶段,虚表就可以构造出来了。

虚表是属于类的,⽽不是属于某个具体的对象,⼀个类只需要⼀个虚表即可。同⼀个类的所有对象都使⽤同⼀个虚表。

为了指定对象的虚表,对象内部包含⼀个虚表的指针,来指向⾃⼰所使⽤的虚表。为了让每个包含虚表的类的对象都拥有⼀个虚表指针,编译器在类中添加了⼀个指针,*__vptr,⽤来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会⾃动被设置为指向类的虚表。

上⾯指出,⼀个继承类的基类如果包含虚函数,那个这个继承类也有拥有⾃⼰的虚表,故这个继承类的对象也包含⼀个虚表指针,⽤来指向它的虚表。

对象的虚表指针⽤来指向⾃⼰所属类的虚表,虚表中的指针会指向其继承的最近的⼀个类的虚函数。

6. 编译器处理虚函数

如果类中有 虚函数,就将虚函数的地址记录在类的虚函数表中 。派⽣类在继承基类的时候,如果有重写基类的虚函数,就将 虚函数表中相应的函数指针设置为派⽣类的函数地址,否则指向基类的函数地址 。为每个类的实例添加⼀个虚表指针(vptr),虚表指针指向类的虚函数表。实例在调⽤虚函数的时候,通过这个虚函数表指针找到类中的虚函数表,找到相应的函数进⾏调⽤。

7. 基类的析构函数⼀般写成虚函数的原因

⾸先析构函数可以为虚函数,当析构⼀个指向⼦类的⽗类指针时, 编译器可以根据虚函数表寻找到⼦类的析构函数进⾏调⽤ ,从⽽正确 释放⼦类对象的资源

如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除指向⼦类的⽗类指针时,只会调⽤⽗类的析构函数⽽不调⽤⼦类析构函数,这样就会造 成⼦类对象析构不完全造成内存泄漏

8. 构造函数为什么⼀般不定义为虚函数

1)因为创建⼀个对象时需要确定对象的类型,⽽虚函数是在运⾏时确定其类型的。⽽在构造⼀个对象时,由于对象还未创建成功,编译器⽆法知道对象的实际类型,是类本身还是类的派⽣类等等

2)虚函数的调⽤需要虚函数表指针,⽽该指针存放在对象的内存空间中;若构造函数声明为虚函数,那么由于对象还没有实例化,还没有内存空间,更没有虚函数表地址⽤来调⽤虚函数即构造函数了。

Person p = new Person(); // 实例化对象,分配空间

9. 构造函数或者析构函数中调⽤虚函数会怎样

构造函数调用顺序:父类->子类

析构函数调用顺序:子类->父类

在构造函数中调⽤虚函数,由于当前对象还没有构造完成,此时调⽤的虚函数指向的是 基类的函数实现 ⽅式。

析构函数中调⽤虚函数,此时调⽤的是⼦类的函数实现⽅式

10. 纯虚函数

纯虚函数是只有声明没有实现的虚函数 ,是对⼦类的约束, 是接⼝继承包含纯虚函数的类是抽象类,它不能被实例化,只有实现了这个纯虚函数的⼦类才能⽣成对象。

使⽤场景:当这个类本身产⽣⼀个实例没有意义的情况下,把这个类的函数实现为纯虚函数,⽐如动物可以派⽣出⽼⻁兔⼦,但是实例化⼀个动物对象就没有意义。 并且可以规定派⽣的⼦类必须重写某些函数的情况下可以写成纯虚函数

11. 静态绑定和动态绑定

  • 静态类型:对象在声明时采用的类型,在编译期既已确定;
  • 动态类型:通常是指一个指针或引用目前所指对象的类型,是在运行期决定的;
  • 静态绑定:绑定的是静态类型,所对应的函数或属性依赖于对象的静态类型,发生在 编译期
  • 动态绑定:绑定的是动态类型,所对应的函数或属性依赖于对象的动态类型, 发生在运行期,虚函数就是动态绑定的。

至此总结一下静态绑定和动态绑定的区别:
1. 静态绑定发生在编译期,动态绑定发生在运行期;

2. 对象的动态类型可以更改,但是静态类型无法更改;

3. 要 想实现动态,必须使用动态绑定;

4. 在继承体系中只有 虚函数使用的是动态绑定 ,其他的全部是静态绑定;

参考:

C++中的静态绑定和动态绑定 - lizhenghn - 博客园

12. 深拷⻉和浅拷⻉

浅拷⻉就是 将对象的指针 进⾏简单的 复制 ,原对象和副本指向的是相同的资源。

⽽深拷⻉是 新开辟⼀块空间 ,将原对象的资源复制到新的空间中,并返回该空间的地址。

深拷⻉可以避免重复释放和写冲突。例如使⽤ 浅拷⻉ 的对象进⾏释放后,对原对象的释放会导致 内存泄漏或程序崩溃

13. 对象复⽤的了解,零拷⻉的了解

对象复⽤指得是设计模式,对象可以采⽤不同的设计模式达到复⽤的⽬的,最常⻅的就是继承和组合模式了。

零拷⻉指的是在进⾏操作时 ,避免CPU从⼀处存储拷⻉到另⼀处存储 。在Linux中,我们可以减少数据在内核空间和⽤户空间的来回拷⻉实现,⽐如通过调⽤mmap()来代替read调⽤。⽤程序调⽤mmap(),磁盘上的数据会通过DMA被拷⻉的内核缓冲区,接着操作系统会把这段内核缓冲区与应⽤程序共享,这样就不需要把内核缓冲区的内容往⽤户空间拷⻉。应⽤程序再调⽤write(),操作系统直接将内核缓冲区的内容拷⻉到socket缓冲区中,这⼀切都发⽣在内核态,最后,socket缓冲区再把数据发到⽹卡去。

14. c++所有的构造函数

类的对象被创建时,编译系统为对象 分配内存空间 ,并⾃动调⽤构造函数, 由构造函数完成成员的初始化⼯作。

即构造函数的作⽤:初始化对象的数据成员。

默认构造函数 是当类没有实现⾃⼰的构造函数时, 编译器默认提供的⼀个构造函数

重载构造函数 也称为⼀般构造函数, ⼀个类可以有多个重载构造函数 ,但是需要参 数类型或个数不相同 。可以在重载构造函数中⾃定义类的初始化⽅式。

拷⻉构造函数 是在发⽣对象复制的时候调⽤的。具体可以参考:

对象以值传递的⽅式传⼊函数参数

如 void func(Dog dog){};

对象以值传递的⽅式从函数返回

如 Dog func(){ Dog d; return d;}

对象需要通过另外⼀个对象进⾏初始化


什么情况下会调⽤拷⻉构造函数(三种情况)

类的对象需要拷⻉时,拷⻉构造函数将会被调⽤,以下的情况都会调⽤拷⻉构造函数:

⼀个对象以值传递的⽅式传⼊ 函数体,需要拷⻉构造函数创建⼀个临时对象压⼊到栈空间中。

⼀个对象以值传递的⽅式从函数返回 ,需要执⾏拷⻉构造函数创建⼀个临时对象作为返回值。

⼀个对象需要通过另外⼀个对象进⾏初始化


为什么拷⻉构造函数必需是引⽤传递,不能是值传递?

为了防⽌递归调⽤。当⼀个对象需要以值⽅式进⾏传递时,编译器会⽣成代码调⽤它的拷⻉构造函数⽣成⼀个副本,如果类 A 的拷⻉构造函数的参数不是引⽤传递,⽽是采⽤值传递,那么就⼜需要为了创建传递给拷⻉构造函数的参数的临时对象,⽽⼜⼀次调⽤类 A 的拷⻉构造函数,这就是⼀个⽆限递归。

15. 结构体内存对⻬⽅式和为什么要进⾏内存对⻬?

因为结构体的成员可以有不同的数据类型,所占的⼤⼩也不⼀样 。同时,由于CPU读取数据是 按块读取 的, 内存对⻬可以使得CPU⼀次就可以将所需的数据读进来

对⻬规则:

第⼀个成员在与结构体变量偏移量为0的地址其他成员变量要对⻬到某个数字(对⻬数)的整数倍的地址处。对⻬数=编译器默认的⼀个对⻬数 与 该成员⼤⼩的较⼩值。

linux 中默认为4

vs 中的默认值为8

结构体总⼤⼩为最⼤对⻬数的整数倍(每个成员变量除了第⼀个成员都有

⼀个对⻬数)

16. 内存泄露的定义,如何检测与避免?

动态分配内存所开辟的空间,在使⽤完毕后未⼿动释放,导致⼀直占据该内存,即为内存泄漏。

⼏种原因:

类的构造函数和析构函数中new和delete没有配套;

在释放对象数组时没有使⽤delete[],使⽤了delete;

没有将基类的析构函数定义为虚函数,当基类指针指向⼦类对象时,如果基类的析构函数不是virtual,那么⼦类的析构函数将不会被调⽤,⼦类的资源没有正确释放,因此造成内存泄露。

没有正确的清楚嵌套的对象指针

避免⽅法:

malloc/free要配套

使⽤智能指针;

将基类的析构函数设为虚函数;

悬挂指针与野指针的区别

悬挂指针:当指针所指向的对象被释放,但是该指针没有任何改变,以至于其仍然指向已经被回收的内存地址,这种情况下该指针被称为悬挂指针;

野指针:未初始化的指针被称为野指针。

指针常量与常量指针的区别

int const *p1 = &b; //const 在前,定义为常量指针
int *const p2 = &c; // *在前,定义为指针常量
指针和 const 谁在前先读谁 ;
*象征着地址,const象征着内容;
谁在前面谁就不允许改变。
理解这些声明的技巧在于,查看关键字const右边来确定什么被声明为常量 ,如果该关键字的
右边是类型,则值是常量;如果关键字的右边是指针变量,则指针本身是常量。

常量指针是指指向常量的指针,顾名思义,就是指针指向的是常量,即,它不能指向变量,它指向的内容不能被改变,不能通过指针来修改它指向的内容,但是指针自身不是常量,它自身的值可以改变,从而指向另一个常量。

指针常量是指指针本身是常量。它指向的地址是不可改变的,但地址里的内容可以通过指针改变。它指向的地址将伴其一生,直到生命周期结束。有一点需要注意的是,指针常量在定义时必须同时赋初值。

17. c++智能指针

C++中的智能指针有auto_ptr,shared_ptr,weak_ptr和unique_ptr。 智能指针其实是将指针进⾏了封装,可以像普通指针⼀样进⾏使⽤,同时可以⾃⾏进⾏释放,避免忘记释放指针指向的内存地址造成内存泄漏。

auto_ptr 是较早版本的智能指针,在进⾏指针拷⻉和赋值的时候, 新指针直接接管旧指针的资源并且将旧指针指向空 ,但是这种⽅式在需要访问旧指针的时候,就会出现问题。

auto_ptr<std::string> p1 (new string ("hello"));
auto_ptr<std::string> p2;
p2 = p1; //auto_ptr 不会报错.
此

此时不会报错,p2 剥夺了 p1 的所有权,但是当程序运⾏时访问 p1 将会报错。所以 auto_ptr的缺点是:存在潜在的内存崩溃问题!

unique_ptr 是auto_ptr的⼀个改良版, 不能赋值也不能拷⻉ ,保证⼀个对象同⼀时间 只有⼀个智能指针

unique_ptr<string> p3 (new string (auto));//#4
unique_ptr<string> p4;//#5
p4 = p3;//此时会报错

编译器认为 p4=p3 ⾮法,避免了 p3 不再指向有效数据的问题。因此,unique_ptr ⽐ auto_ptr 更安全。

shared_ptr 可以使得⼀个对象可以 有多个智能指针 ,当这个对象所有的智能指针被销毁时就会⾃动进⾏回收。(内部使⽤计数机制进⾏维护)

weak_ptr是为了协助shared_ptr⽽出现的 。它不能访问对象 ,只能观测shared_ptr的引⽤计数,防⽌出现死锁。

补充:

share_ptr原理:

shared_ptr是可以共享所有权的指针。如果有多个shared_ptr共同管理同⼀个对象时,只有这些shared_ptr全部与该对象脱离关系之后,被管理的对象才会被释放。

shared_ptr的管理机制其实并不复杂,就是对所管理的对象进⾏了引⽤计数,当新增⼀个shared_ptr对该对象进⾏管理时,就将该对象的引⽤计数加⼀;减少⼀个shared_ptr对该对象进⾏管理时,就将该对象的引⽤计数减⼀,如果该对象的引⽤计数为0的时候,说明没有任何指针对其管理,才调⽤delete释放其所占的内存。

参考:

[C++] Boost智能指针--boost::shared_ptr(使用及原理分析)

18. inline关键字说⼀下 和宏定义有什么区别

函数的调用,想必大家都用过,一个函数在被另一个函数调用的时候,才有生命,才会为其准备对应的内存空间,再调用完毕之后再清理释放结束。

可以看到,每一次的函数调用都会带来一些时间和空间上的花销。

而自定义函数的一个作用,也是为了提高代码的重用性,可以在需要的时候随时调用,提高开发效率。那么, 一个代码本身就不多,又频繁被调用的函数 ,我们就该好好想想,这样做到底合算不合算了。

好在,C++已经帮我们考虑到这个问题,为我们提供了内联的机制,即仍然使用自定义函数,但在编译的时候,把函数代码插入到函数调用处,从而免去函数调用的一系列过程,像普通顺序执行的代码一样,来解决这个问题!

复制代码
#include<iostream>
using namespace std;
inline int Max(int a,int b)
    return a>b?a:b;
int main()
    cout<<Max(3,5)<<endl;
    cout<<Max(7,9)<<endl;
    return 0;
}

那么用法呢,也非常简单,只需要在函数定义的前面加上关键字inline声明就可以了

值得说明的是, 内联函数的定义要在调用之前出现,才可以让编译器在编译期间了解上下文,进行代码替换。

除此以外,内联函数与register变量类似,仅仅是我们提给编译器的一个请求,最终是否真正会实现内联,由编译器根据情况自行选择。

19. 与宏的比较

通常,在 C语言 中,内联展开的功能由带参宏(Macros)在源码级实现。内联提供了几个更好的方法:

  • 宏(define)调用并不执行 类型检查 ,甚至连正常参数也不检查,但是函数调用却要检查。
  • C语言的宏使用的是文本替换,可能导致无法预料的后果,因为需要重新计算参数和 操作顺序
  • 在宏中的编译错误很难发现,因为它们引用的是扩展的代码,而不是程序员键入的。
  • 许多结构体使用宏或者使用不同的语法来表达很难理解。内联函数使用与普通函数相同的语言,可以随意的内联和不内联。
  • 内联代码的调试信息通常比扩展的宏代码更有用。 [1]

20. 内联函数的不足

除了通常使用内联扩展可能带来的问题,作为一种编程语言特性的内联函数也可能并没有看起来那么有效,原因如下:

  • 通常,设计编译器的程序设计者比大多数的程序设计者更清楚对于一个特定的函数是否合适进行内联扩展;一些情况下,对于程序员指定的某些内联函数,编译器可能更倾向于不使用内联甚至根本无法完成内联。
  • 对于一些开发中的函数,它们可能从原来的不适合内联扩展变得适合或者倒过来。尽管内联函数或者非内联函数的转换易于宏的转换,但增加的维护开支还是使得它的优点显得更不突出了。
  • 对于基于C的编译系统,内联函数的使用可能大大增加编译时间,因为每个调用该函数的地方都需要替换成函数体, 代码量的增加也同时带来了潜在的编译时间的增加 。 [1]

内联函数和宏定义的差别:

内联函数和普通函数相比可以加快程序运行速度,因为不需要中断调用,在编译的时候内联函数可以直接被镶嵌到目标代码中,而宏只是一个简单的替换。

内联函数需要做参 数类型检查 ,宏不需要

一般内联函数用于以下情况

(1)一个函数被重复不断调用

(2)函数知识简单的几行,且函数内不包含for、while、switch语句。

宏不是函数,只是在编译前(编译预处理阶段)将程序中有关字符串替换成宏体。

反之,如果函数体内代码执行的时间要比函数调用的开销大,则不需要使用内联函数,内联函数是以代码膨胀(复制)为代价,将使程序的代码总量增加,消耗更多的内存空间,

因此一般内联函数不用于以下情况

(1) 函数体内代码比较长,内联会导致内存空间消耗大

(2)函数体内出现循环,执行函数体内代码时间要比函数调用开销大。

21. 模板的⽤法与适⽤场景 实现原理

⽤template <typename t="">关键字进⾏声明,接下来就可以进⾏模板函数和模板类的编写了</typename>

编译器会对函数模板进⾏两次编译: 第⼀次编译 声明 的地⽅对模板代码本身进⾏编译,这次编译只会进⾏⼀个 语法检查 ,并不会⽣成具体的代码。 第⼆次编译 时对代码进⾏ 参数替换 后再进⾏编译, ⽣成具体的函数代码

22. 成员初始化列表的概念,为什么⽤成员初始化列表会快⼀些(性能优势)

成员初始化列表就是在类或者结构体的构造函数(牛客上 链表或者树的结构体定义时就使用成员初始化函数)中,在参数列表后以冒号开头,逗号进⾏分隔的⼀系列初始化字段。

class class A{
int int id;
string name;
FaceImage face;
A(int int& inputID,string& inputName,FaceImage& inputFace):id(inputID),name(inputName),face(inputFace){} 
};

因为使⽤成员初始化列表进⾏初始化的话, 会直接使⽤传⼊参数的拷⻉构造函数进⾏初始化 省去 了⼀次执⾏传⼊参数的 默认构造函数的过程 ,否则会调⽤⼀次传⼊参数的默认构造函数。所以使⽤成员初始化列表效率会⾼⼀些。另外,有三种情况是必须使⽤成员初始化列表进⾏初始化的:

常量成员的初始化,因为常量成员只能初始化不能赋值

引⽤类型

没有默认构造函数的对象必须使⽤成员初始化列表的⽅式进⾏初始化

参考:

C++ 初始化列表 - 翰墨小生 - 博客园

23. C++的调⽤惯例(简单⼀点C++函数调⽤的压栈过程)

函数的调⽤过程

1) 从栈空间分配存储空间(因为形参是在栈空间的,所以先为形参分配栈空间,为下一步做准备)

2)从实参的存储空间复制值到形参栈空间

3)进⾏运算

形参在函数未调⽤之前都是没有分配存储空间的,在函数调⽤结束之后,形参弹出栈空间,清除形参空间。

数组作为参数的函数调⽤⽅式是地址传递,形参和实参都指向相同的内存空间,调⽤完成后,形参指针被销毁,但是所指向的内存空间依然存在,不能也不会被销毁。

当函数有多个返回值的时候,不能⽤普通的 return 的⽅式实现,需要通过传回地址的形式进⾏,即地址/指针传递。

24. C++的四种强制转换

四种强制类型转换操作符分别为:static_cast、dynamic_cast、const_cast、reinterpret_cast

1)static_cast :

⽤于各种隐式转换。具体的说,就是⽤户各种基本数据类型之间的转换,⽐如把int换成char,float换成int等。以及派⽣类(⼦类)的指针转换成基类(⽗类)指针的转换。

特性与要点:

1. 它 没有运⾏时类型检 查,所以是有安全隐患的。

2. 在派⽣类指针转换到基类指针时,是没有任何问题的,在基类指针转换到派⽣类指针的时候,会有安全问题。

3. static_cast不能转换const,volatile等属性

2)dynamic_cast:

专门用于派生类之间的转换 。具体的说,就是在基类指针到派⽣类指针,或者派⽣类到基类指针的转换。

dynamic_cast 能够提供运⾏时类型检查,只⽤于含有虚函数的类

dynamic_cast如果不能转换返回NULL。

3)const_cast:

专⻔⽤于 const 属性的转换 ,去除 const 性质,或增加 const 性质, 是四个转换符中唯⼀⼀个可以操作敞亮的转换符。

4)reinterpret_cast

⼏乎什么都可以转,⽤在任意的指针之间的转换,引⽤之间的转 换,不到万不得已,不要使⽤这个转换符,⾼危操作。使⽤特点: 从底层对数据进⾏重新解释,依赖具体的平台,可移植性差; 可以将整形转 换为指针,也可以把指针转换为数组;可以在指针和引⽤之间进⾏肆⽆忌惮的转换。

25. ⼀个函数或者可执⾏⽂件的⽣成过程或者编译过程是怎样的

预处理、编译、汇编、链接、装载、运行

1)预处理 : 对预处理命令进⾏替换等预处理操作

主要处理源代码⽂件中的以“#”开头的预编译指令。处理规则⻅下

1、删除所有的#define,展开所有的宏定义。

2、处 理所有的条件预编译指 令,如“#if”、“#endif”、“#ifdef”、“#elif”和“#else”。

3、处理“#include”预编译指令,将⽂件内容替换到它的位置,这个过程是递归进⾏的,⽂件中包含其他⽂件。

4、 删除所有的注释 ,“//”和“/**/”。5、保留所有的#pragma 编译器指令,编译器需要⽤到他们,如:#pragma once 是为了防⽌有⽂件被重复引⽤。

6、添加⾏号和⽂件标识,便于编译时编译器产⽣调试⽤的⾏号信息,和编译时产⽣编译错误或警告是能够显示⾏号。

2)编译:代码优化和⽣成汇编代码

把预编译之后⽣成的xxx.i或xxx.ii⽂件,进⾏⼀系列词法分析、语法分析、语义分析及优化后,⽣成相应的汇编代码⽂件xxx.s。

1、 词法分析 :利⽤类似于“有限状态机”的算法,将源代码程序输⼊到扫描机中,将其中的字符序列分割成⼀系列的记号。

2、 语法分析 :语法分析器对由扫描器产⽣的记号,进⾏语法分析,产⽣语法树。由语法分析器输出的语法树是⼀种以表达式为节点的树。

3、 语义分析 :语法分析器只是完成了对表达式语法层⾯的分析,语义分析器则对表达式是否有意义进⾏判断,其分析的语义是静态语义——在编译期能分期的语义,相对应的动态语义是在运⾏期才能确定的语义。

4、优化:源代码级别的⼀个优化过程。

5、⽬标代码⽣成:由代码⽣成器将中间代码转换成⽬标机器代码,⽣成⼀系列的代码序列——汇编语⾔表示。

6、⽬标代码优化:⽬标代码优化器对上述的⽬标机器代码进⾏优化:寻找合适的寻址⽅式、使⽤位移来替代乘法运算、删除多余的指令等。

3)汇编:将汇编代码转化为机器语⾔

将汇编代码转变成机器可以执⾏的指令(机器码⽂件)。 汇编器的汇编过程相对于编译器来说更简单,没有复杂的语法,也没有语义,更不需要做指令优化,只是根据汇编指令和机器指令的对照表⼀⼀翻译过来,汇编过程有汇编器完成。经汇编之后,产⽣⽬标⽂件(与可执⾏⽂件格式⼏乎⼀样)xxx.o(Windows下)、xxx.obj(Linux下)。

4)链接:将⽬标⽂件彼此链接起来

将不 同的源⽂件产⽣的⽬标⽂件进⾏链接,从⽽形成⼀个可以执⾏的程序 。链接分为静态链接和动态链接:

1、静态链接:

函数和数据被编译进⼀个⼆进制⽂件。在使⽤静态库的情况下,在编译链接可执⾏⽂件时,链接器从库中复制这些函数和数据并把它们和应⽤程序的其它模块组合起来创建最终的可执⾏⽂件。

空间浪费:因为每个可执⾏程序中对所有需要的⽬标⽂件都要有⼀份副本,所以如果多个程序对同⼀个⽬标⽂件都有依赖,会出现同⼀个⽬标⽂件都在内存存在多个副本;更新困难:每当库函数的代码修改了,这个时候就需要重新进⾏编译链接形成可执⾏程序。

运⾏速度快:但是静态链接的优点就是,在可执⾏程序中已经具备了所有执⾏程序所需要的任何东⻄,在执⾏的时候运⾏速度快。

2、动态链接:

动态链接的基本思想是把程序按照模块拆分成各个相对独⽴部分,在程序运⾏时才将它们链接在⼀起形成⼀个完整的程序,⽽不是像静态链接⼀样把所有程序模块都链接成⼀个单独的可执⾏⽂件。

共享库:就是即使需要每个程序都依赖同⼀个库,但是该库不会像静态链接那样在内存中存在多分,副本,⽽是这多个程序在执⾏时共享同⼀份副本;更新⽅便:更新时只需要替换原来的⽬标⽂件,⽽⽆需将所有的程序再重新链接⼀遍。当程序下⼀次运⾏时,新版本的⽬标⽂件会被⾃动加载到内存并且链接起来,程序就完成了升级的⽬标。

性能损耗:因为把链接推迟到了程序运⾏时,所以每次执⾏程序都需要进⾏链接,所以性能会有⼀定损失。

进程的加载过程:

进程的执⾏过程需要经过三⼤步骤:编译,链接和装⼊。

编译:将源代码编译成若⼲模块;

链接:将编译后的模块和所需的库函数进⾏链接。

链接包括三种形式:静态链接,装⼊时动态链接(将编译后的模块在链接时⼀边链接⼀边装⼊),运⾏时动态链接(在执⾏时才把需要的模块进⾏链接)

装⼊ :将模块装⼊内存运⾏

将进程装⼊内存时,通常使⽤分⻚技术,将内存分成固定⼤⼩的⻚,进程分为固定⼤⼩的块,加载时将进程的块装⼊⻚中,并使⽤⻚表记录。减少外部碎⽚。通常操作系统还会使⽤虚拟内存的技术将磁盘作为内存的扩充。

静态链接和动态链接的区别

静态链接是在编译链接时直接将需要的执行代码拷贝到调用处;

优点在于程序在发布时不需要依赖库,可以独立执行,

缺点在于程序的体积会相对较大,而且如果静态库更新之后,所有可执行文件需要重新链接;

动态链接是在编译时不直接拷贝执行代码,而是通过记录一系列符号和参数,在程序运行或加载时将这些信息传递给操作系统,操作系统负责将需要的动态库加载到内存中,然后程序在运行到指定代码时,在共享执行内存中寻找已经加载的动态库可执行代码,实现运行时链接;

优点在于多个程序可以共享同一个动态库,节省资源;

缺点在于由于运行时加载,可能影响程序的前期执行性能。

26. C11新特性

⾃动类型推导auto: auto的⾃动类型推导⽤于从初始化表达式中推断出变量的数据类型。通过auto的⾃动类型推导,可以⼤⼤简化我们的编程⼯作。

nullptr:nullptr是为了解决原来C++中NULL的⼆义性问题⽽引进的⼀种新的类型,因为NULL实际上代表的是0,⽽nullptr是void*类型的。

lambda表达式 :它类似Javascript中的闭包,它可以⽤于创建并定义匿名的函数对象,以简化编程⼯作。Lambda的语法如下:

函数对象参数mutable或exception声明->返回值类型{函数体}。

thread类和mutex类。

新的智能指针 unique_ptr和shared_ptr。


27. c和c++编程差异

(1)C语言和C++的结构体声明都可以使用

struct Node{
    #some code
};

但是在创建结构体类型的变量时,C语言必须使用struct Node varName;而C++可以省略struct关键字,直接Node varName。

如果想要在C语言中单独使用Node varName创建一个结构体的话,可以在定义结构体的时候使用typedef关键字,比如:

typedef Struct Node{
    #somecode....
}Node;

因为typedef的作用就是定义类型别名,上面的第一个Node(其实应该将struct Node看成一个整体)是定义的一个结构体,而后面的一个Node,则是前面struct Node的别名,即Node==struct Node。

(2)C语言中的malloc函数(stdlib.h),如果要申请一个结构体的内存,下面是两种语言的区别:

L = (LinkList)malloc(sizeof(struct Node)); //C语言必须这样,C++可以这样
L = (LinkList)malloc(sizeof(Node));//C++也可以这样

对于使用malloc申请其他基本类型(int,double,char,float,double)的内存空间,两种语言的是一样的

28. static关键字在C语言和C++中各自有哪些不同用法?

参考: 关键字static在C和C++中的区别 - 山路水桥 - 博客园

首先,C++是C的超集,所以static在C中的用法 对于C++来说是全盘接受的,而两者的不同也就是C++中多出来的特性,而这些多出来的特性与C++面向对象的特性有关,或更具体的说,就是static在“类”中的意义和作用。

在 C 中 static 用来修饰局部静态变量和外部静态变量、函数。而 C++中除了上述功能外,还用来定义类的成员变量和函数。即静态成员和静态成员函数。



29. union是什么,有什么用

有点和结构体类似,不同的是结构体的长度是对齐后的长度,union的长度是最长变量的长度。里面的值也是采用覆盖技术,就是三个值是一样的。


30. volatil和extern关键字

volatile 关键字提醒编译器: a 可能随时被意外修改,意外的意思是虽然当前这段代码里看起来 a 不会变,但可能别的地方正在修改 a 的值,所谓"别的地方",某些情况下指的就是其他线程了。多线程的时候被共享的变量。

volatile 三个特性

易变性 :在汇编层⾯反映出来,就是两条语句,下⼀条语句不会直接使⽤上⼀条语句对应的volatile 变量的寄存器内容, ⽽是重新从内存中读取

不可优化性 :volatile 告诉编译器, 不要对 我这个变量进⾏各种激进的 优化 ,甚⾄将变量直接消除,保证程序员写在代码中的指令,⼀定会被执⾏。

顺序性 :能够保证 volatile 变量之间的顺序性 ,编译器不会进⾏乱序优化。

因为volatile变量用于多线程的时候,你给他优化没了,不久尴尬了嘛

extern

在 C 语⾔中,修饰符 extern ⽤在变量或者函数的声明前, ⽤来说明 “此变量/函数是在别处定义的,要在此处引⽤”。

注意 extern 声明的位置对其作⽤域也有关系,如果是在 main 函数中进⾏声明的,则只能在main 函数中调⽤,在其它函数中不能调⽤。其实要调⽤其它⽂件中的函数和变量,只需把⽂件⽤ #include 包含进来即可,为啥要⽤ extern? 因为⽤ extern 会加速程序的编译过程 ,这样能节省时间。

在 C++ 中 extern 还有另外⼀种作⽤, ⽤于指示 C 或者 C++函数的调⽤规范 。⽐如在C+ + 中调⽤ C 库函数,就需要在 C++ 程序中⽤ extern “C” 声明要引⽤的函数。这是给链接器⽤的,告诉链接器在链接的时候⽤C 函数规范来链接。主要原因是 C++ 和 C 程序编译完成后在⽬标代码中命名规则不同,⽤此来解决名字匹配的问题。

参考:

C/C++中volatile关键字详解 - chao_yu - 博客园


31. 右值引用是什么,move是为了解决什么问题?

(1)左值引用绑定到有确定存储空间以及变量名的对象上,表达式结束后对象依然存在;右值引用绑定到要求转换的表达式、字面常量、返回右值的表达式等临时对象上,赋值表达式结束后就对象就会被销毁。

(2)左值引用后可以利用别名修改左值对象;右值引用绑定的值不能修改。

右值引用的主要目的是为了实现转移语义和完美转发,消除两个对象交互时不必要的对象拷贝,也能够更加简洁明确地定义泛型函数。

move是为了将左值引用转换为右值引用。



++I和I++的区别

++i (前置加加)先⾃增 1再返回,i++ (后置加加)先返回 i 再⾃增1。前置加加不会产⽣临时对象,后置加加必须产⽣临时对象,临时对象会导致效率降低

++i 实现:

int& int::operator++ (){
*this +=1;