寒武纪软件开发面试题C++

寒武纪软件开发面试题C++

    • 1 虚函数
    • 2 C++纯虚函数
    • 3 抽象类
    • 4 虚函数与纯虚函数总结
    • 5 C++虚函数表剖析
    • 6 C++三种继承方式下的访问权限控制
    • 7 C++ 宏定义与内嵌函数
    • 8 new、malloc的区别
    • 9 堆/栈的区别?
    • 10 从源文件到可执行文件的过程?
    • 11 静态链接与动态链接的区别
    • 12 C++内存机制中内存溢出、内存泄露、内存越界和栈溢出的区别和联系
    • 13 C++中的类型转换
    • 14 构造函数和析构函数的作用是什么?什么时候需要自己定义构造函数和析构函数?
    • 15 C++中指针和引用的区别
    • 16 static关键字的作用
    • 17 null与nullptr区别
    • 18 C/C++中volatile关键字详解
    • 19 C++11线程中的几种锁
    • 20 STL中map和unordered_map 区别、
    • 21 哈希表介绍一下,冲突解决
    • 22 面向对象,封装,继承,多态,介绍一下,这三者的意义分别是什么,这样做优势在哪?
    • 23 C++的智能指针
    • 23* shared_ptr引发的线程安全问题:
    • 23 ** shared_ptr所导致的循环引用的问题
    • 24 【C++】浅拷贝和深拷贝
    • 25 * STL的分类
    • 25 list和vector有什么区别?
    • 26 构造函数和析构函数能否为虚函数
    • 27 C++ 内存管理
    • 1 静态区域
    • 2 动态区域
    • 3 区分堆和栈
    • 堆和栈的区别
    • 28 C++中的静态多态和动态多态
    • 29 malloc 底层实现与原理
    • 双向链表的插入

1 虚函数

  1. 定义一个函数为虚函数,不代表函数为不被实现的函数。(虚函数是可以被实现的
  2. 定义他为虚函数是为了允许用基类的指针来调用子类的这个函数。(主要用于实现运行时多态
  3. 定义一个函数为纯虚函数,才代表函数没有被实现。(纯虚函数是不被实现的
  4. 定义纯虚函数是为了实现一个接口,起到一个规范的作用规范继承这个类的程序员必须实现这个函数。

1、简介

class A
{
public:
virtual void foo()
{
cout<<"A::foo() is called"<<endl;
}
};
class B:public A
{
public:
void foo()
{
cout<<"B::foo() is called"<<endl;
}
};
int main(void)
{
A *a = new B();
a->foo(); // 在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B的!
return 0;
}

这个例子是虚函数的一个典型应用,通过这个例子,也许你就对虚函数有了一些概念。它虚就虚在所谓“推迟联编”或者“动态联编”上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被成为“虚”函数。
虚函数只能借助于指针或者引用来达到多态的效果。

2 C++纯虚函数

一、定义
纯虚函数是在基类中声明的虚函数,它在基类中没有定义,但要求任何派生类都要定义自己的实现方法。在基类中实现纯虚函数的方法是在函数原型后加“=0”

virtual void funtion1()=0

二、引入原因

  1. 为了方便使用多态特性,我们常常需要在基类中定义虚拟函数
  2. 在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。
    2.1 为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类它不能生成对象。这样就很好地解决了上述两个问题。
    2.2 纯虚函数最显著的特征是:它们必须在继承类中重新声明函数(不要后面的=0,否则该派生类也不能实例化),而且它们在抽象类中往往没有定义。
    2.3 定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口
    2.4 纯虚函数的意义,让所有的类对象(主要是派生类对象)都可以执行纯虚函数的动作,但类无法为纯虚函数提供一个合理的缺省实现。所以类纯虚函数的声明就是在告诉子类的设计者,“你必须提供一个纯虚函数的实现,但我不知道你会怎样实现它”。

3 抽象类

抽象类是一种特殊的类,它是为了抽象和设计的目的为建立的,它处于继承层次结构的较上层(基类)。
(1)抽象类的定义:
称带有纯虚函数的类为抽象类。
(2)抽象类的作用:
抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根派生类将具体实现在其基类中作为接口的操作。所以派生类实际上刻画了一组子类的操作接口的通用语义,这些语义也传给子类,子类可以具体实现这些语义,也可以再将这些语义传给自己的子类。(其实就是定义接口
(3)使用抽象类时注意:

  1. 抽象类只能作为基类来使用,其纯虚函数实现由派生类给出如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。
  2. 抽象类是不能定义对象的。

4 虚函数与纯虚函数总结

  1. 纯虚函数声明如下: virtual void funtion1()=0; 纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。
  2. 虚函数声明如下:virtual ReturnType FunctionName(Parameter);虚函数必须实现,如果不实现,编译器将报错
  3. 对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定
  4. 实现了纯虚函数的子类,该纯虚函数在子类中就编程了虚函数,子类的子类即孙子类可以覆盖该虚函数,由多态方式调用的时候动态绑定。
  5. 虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数
  6. 在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚的。
  7. 析构函数应当是虚函数,将调用相应对象类型的析构函数,因此,如果指针指向的是子类对象,将调用子类的析构函数,然后自动调用基类的析构函数

有纯虚函数的类是抽象类,不能生成对象,只能派生。他派生的类的纯虚函数没有被改写,那么,它的派生类还是个抽象类。
定义纯虚函数就是为了让基类不可实例化化
因为实例化这样的抽象数据结构本身并没有意义。
或者给出实现也没有意义
实际上我个人认为纯虚函数的引入,是出于两个目的
1、为了安全,因为避免任何需要明确但是因为不小心而导致的未知的结果,提醒子类去做应做的实现。
2、为了效率,不是程序执行的效率,而是为了编码的效率。

5 C++虚函数表剖析

https://blog.csdn.net/lihao21/article/details/50688337
摘要出的关键信息:

  1. 实现C++的多态,C++使用了一种动态绑定的技术。这个技术的核心是虚函数表
  2. 每个包含了虚函数的类都包含一个虚表
  3. 一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表
  4. 虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。

6 C++三种继承方式下的访问权限控制

1 C++类中的成员(函数/变量)拥有三种访问权限:

  • public: 用该关键字修饰的成员表示公有成员,该成员不仅可以在类内可以被访问,在类外也是可以被访问的,是类对外提供的可访问接口;
  • private: 用该关键字修饰的成员表示私有成员,该成员仅在类内可以被访问,在类体外是隐藏状态;
  • protected: 用该关键字修饰的成员表示保护成员,保护成员在类体外同样是隐藏状态,但是对于该类的派生类来说,相当于公有成员,在派生类中可以被访问。

2 不同的继承方式,基类中的成员访问权限:

  • 若继承方式是public,基类成员在派生类中的访问权限保持不变,也就是说,基类中的成员访问权限,在派生类中仍然保持原来的访问权限;

  • 若继承方式是private,基类所有成员在派生类中的访问权限都会变为私有(private)权限

  • 若继承方式是protected, 基类的共有成员和保护成员在派生类中的访问权限都会变为保护(protected)权限,私有成员在派生类中的访问权限仍然是私有(private)权限。

在这里插入图片描述

7 C++ 宏定义与内嵌函数

用内联函数取代宏的优点:

  1. 内联函数在运行时可调试,而宏定义不可以;
  2. 编译器会对内联函数的参数类型做安全检查自动类型转换(同普通函数),而宏定
    义则不会;
  3. 内联函数可以访问类的成员变量,宏定义则不能
  4. 在类中声明同时定义的成员函数,自动转化为内联函数。

内联函数和普通函数相比可以加快程序运行的速度,因为不需要中断调用,在编译的时候内联函数可以直接被镶嵌到目标代码中。
内联函数要做参数类型检查,这是内联函数跟宏相比的优势
inline是指嵌入代码,就是在调用函数的地方不是跳转,而是把代码直接写到那里去。对于短小的代码来说,inline可以带来一定的效率提升,而且和C时代的宏函数相比,inline 更安全可靠。可是这个是以增加空间消耗为代价的。至于是否需要inline函数就需要根据你的实际情况取舍了。

  1. 宏是在代码处不加任何验证的简单替代,而内联函数是将代码直接插入调用处,而减少了普通函数调用时的资源消耗。
  2. 宏不是函数,只是在编译前(编译预处理阶段)将程序中有关字符串替换成宏体
  3. inline函数是函数,但在编译中不单独产生代码,而是将有关代码嵌入到调用处

宏定义示例:#define MAX(a,b) ((a)>(b)?(a):(b))
内联函数定义:

inline int MAX(int a,int b)
    {
     return a>b?a:b;
    }

8 new、malloc的区别

先对new和delete简单进行一下总结,然后再细说new和malloc的区别
一、new和delete
C语言提供了malloc和free两个系统函数,完成对堆内存的申请和释放。而 C++ 则提供了两个关键字new和delete
1.1 规则

  1. new/delete是关键字,效率高于malloc和free。
  2. 配对使用,避免内存泄漏和多重释放。
  3. 避免交叉使用,比如malloc申请空间delete释放,new出的空间被free。
  4. new/delete 主要是用在类对象的申请和释放申请的时候会调用构造器完成初始化释放的时候,会调用析构器完成内存清理

二、new和malloc的区别
2.1 属性
new和deleteC++关键字,需要编译器支持malloc和free是库函数需要头文件支持
2.2 参数
使用new操作符申请内存分配时无须指定内存块的大小编译器会根据类型信息自行计算。而malloc则需要显式地指出所需内存的尺寸
2.3 返回类型
new操作符内存分配成功时,返回的是 对象类型的指针类型严格与对象匹配无须进行类型转换,故new是符合类型安全性的操作符。 而malloc内存分配成功则是返回void *** ,需要通过强制类型转换将void*指针转换成我们需要的类型**。
2.4 自定义类型
new先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。
malloc/free是库函数只能动态的申请和释放内存无法强制要求其做自定义类型对象构造和析构工作
2.5 重载
C++允许重载new/delete操作符,malloc不允许重载。
2.6 内存区域
new做两件事:分配内存和调用类的构造函数,delete是:调用类的析构函数和释放内存。而malloc和free只是分配和释放内存
new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。
2.7 分配失败
new内存分配失败时,会抛出bac_alloc异常。malloc分配内存失败时返回NULL
2.8 内存泄漏
内存泄漏对于new和malloc都能检测出来,而new可以指明是哪个文件的哪一行,malloc确不可以

9 堆/栈的区别?

https://blog.csdn.net/bolinzhiyi/article/details/104984885
1. 管理方式不同:

  1. 对于来讲,是由编译器自动管理,无需我们手工控制;
  2. 对于来说,申请和释放工作由程序员控制,容易产生memory leak。

2. 空间大小不同:

  1. 对于来讲,一般都是有一定的空间大小的,一般默认的栈空间大小为1M,同时,我们是可以进行修改的;
  2. 对于来说,在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。

3. 能否产生碎片不同:

  1. 对于来讲,不会有碎片,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出(具体操作和数据结构中的栈操作是一致的);
  2. 对于来说,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。

4. 生长方向不同:

  1. 对于来讲,生长方向是向下的,是向着内存地址减小的方向增长;(参考C/C++内存布局图)
  2. 对于来说,生长方向是向上的,也就是向着内存地址增加的方向。

5. 分配方式不同:

  1. 对于来讲,栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。;(参考C/C++内存布局图)
  2. 对于来说,堆都是动态分配的,没有静态分配的堆。

6. 分配效率不同:

  1. 对于来讲,栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
  2. 对于来说,堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。

10 从源文件到可执行文件的过程?

流程概况:将源代码转换成机器可识别代码的过程,编译程序读取源代码,对他进行词法和语法的分析,将高级语言转化为功能等效的汇编代码,然后转化为机器语言,按照操作系统对可执行文件格式的要求连接生成可执行程序
源程序->编译预处理->编译->汇编程序->链接程序->可执行文件
在这里插入图片描述
1)预处理阶段
预处理器(cpp)对源程序中以 #开头的命令进行处理 ,例如将#include命令后面的.h文件内容插入程序文件。输出结果是一个以.i为扩展名的源文件hello.i。
读取源代码,对其中的伪指令(#开头)和特殊符号进行处理.主要包括:
宏定义
#define Name TokenString :将所有Name替换成TokenString
#undef: 取消宏定义
条件编译
#ifdef,#ifudef,#else,#elif,endif等等
这些伪指令让程序员通过不同的宏决定编译程序对那些代码进行处理,从而把不必要的代码过滤掉
头文件包含指令
#include “FileName”,#include
特殊符号
预编译可以处理一些特殊符号,比如LINE是当前的行号(十进制),FILE是当前源程序的名称.预编译会识别这些特殊符号.
2)编译阶段
编译器(ccl)对预处理后的源程序进行编译生成一个汇编语言程序hello.s。汇编语言源程序中的每一条语句都以一种文本格式描述了一条低级指令。
3)汇编阶段
汇编器(as)将hello.s 翻译成机器语言指令,把这些指令打包成一个称为可重定位目标文件的hello.o,一种二进制文件,用文本编辑器打开会乱码。
4)链接阶段
链接器(ld)将多个可重定位目标文件和标准库函数合并为一个可执行目标文件, 或简称可执行文件。

11 静态链接与动态链接的区别

我们大家在编程过程中对“链接”这个词并不陌生,链接所解决的问题即是将我们自己写的代码和别人写的库集成在一起。链接可以分为静态链接动态链接,下文将分别讲解这两种方式的特点与其区别。
1、静态链接
1)特点:在生成可执行文件的时候(链接阶段),把所有需要的函数的二进制代码都包含到可执行文件中去。因此,链接器需要知道参与链接的目标文件需要哪些函数,同时也要知道每个目标文件都能提供什么函数,这样链接器才能知道是不是每个目标文件所需要的函数都能正确地链接。如果某个目标文件需要的函数在参与链接的目标文件中找不到的话,链接器就报错了。目标文件中有两个重要的接口来提供这些信息:一个是符号表,另外一个是重定位表。
2) 优点:在程序发布的时候就不需要的依赖库,也就是不再需要带着库一块发布,程序可以独立执行。
3)缺点: 程序
体积会相对大一些
。 如果静态库有
更新
的话,所有可执行文件都得重新链接才能用上新的静态库。

2 、动态链接
1)特点: 在编译的时候不直接拷贝可执行代码,而是通过记录一系列符号和参数,在程序运行或加载时将这些信息传递给操作系统,操作系统负责将需要的动态库加载到内存中,然后程序在运行到指定的代码时,去共享执行内存中已经加载的动态库可执行代码,最终达到运行时连接的目的
2)优点: 多个程序可以共享同一段代码,而不需要在磁盘上存储多个拷贝。
3)缺点: 由于是运行时加载,可能会影响程序的前期执行性能。

上面的文章多次提到库(lib)这个概念,所谓的库就是一些功能代码经过编译连接后的可执行形式。

12 C++内存机制中内存溢出、内存泄露、内存越界和栈溢出的区别和联系

内存溢出(out of memory)

  • 是指程序在申请内存时,没有足够的内存空间供其使用

内存泄漏(memory leak)

  • 是指程序在申请内存后,无法释放已申请的内存空间,占用有用内存

简单理解,内存溢出就是要求分配的内存超出了系统所给的。内存泄漏是指向系统申请分配内存进行使用(new),但是用完后不归还(delete),导致占用有效内存。(内存泄漏最终会导致内存溢出

内存越界

  • 是指向系统申请一块内存后,使用时却超出申请范围。

缓冲区溢出(栈溢出)

  • 程序为了临时存取数据的需要,一般会分配一些内存空间称为缓冲区。如果向缓冲区中写入缓冲区无法容纳的数据,机会造成缓冲区以外的存储单元被改写,称为缓冲区溢出。而栈溢出是缓冲区溢出的一种,原理也是相同的。分为上溢出和下溢出。其中,上溢出是指栈满而又向其增加新的数据,导致数据溢出;下溢出是指空栈而又进行删除操作等,导致空间溢出。

内存泄漏可分为4类:
1.常发性内存泄漏

引起内存泄漏的代码会被很多次执行,每次执行的时候都会导致内存泄漏

2.偶发性内存泄漏

在某些特定的环境下执行引起内存泄漏的代码,才会引起内存泄漏

从以上两种内存泄漏的方式来看,测试环境和测试方法在程序生命周期的重要性是不可或缺的。

3.一次性内存泄漏

代码只会执行一次,但总有一块内存发生泄漏,多见于构造类的时候,析构函数没有释放内存。

4.隐式泄漏

程序运行过程中不断的分配内存,直到结束时才释放内存,但一般服务器程序会运行较长的时间,不及时释放也会导致内存耗尽以至于内存泄漏。

综上所述,一次性内存泄漏对用户的程序维护是没有什么实质性的伤害,但在实际生活中,我们还是尽可能要避免此类的事件发生。

13 C++中的类型转换

c++ 四种强制类型转换介绍
const_cast 、 static_cast 、 dynamic_cast、 reinterpret_cast

1. C风格的强制转换
C风格的强制转换(Type Cast)容易理解,不管什么类型的转换都可以使用使用下面的方式.

TypeName b = (TypeName)a;

当然,C++也是支持C风格的强制转换,但是C风格的强制转换可能带来一些隐患,让一些问题难以察觉.所以C++提供了一组可以用在不同场合的强制转换的函数.
2. C++ 四种强制转换类型函数
2.1 const_cast
1、常量指针被转化成非常量的指针,并且仍然指向原来的对象;
2、常量引用被转换成非常量的引用,并且仍然指向原来的对象;
3、const_cast一般用于修改指针。如const char *p形式。

    // 常量化数组指针
    const int*c_ptr = ary;
    //c_ptr[1] = 233;   //error

    // 通过const_cast 去常量
    int *ptr = const_cast<int*>(c_ptr);

2.2 static_cast
1、static_cast 作用和C语言风格强制转换的效果基本一样,由于没有运行时类型检查来保证转换的安全性,所以这类型的强制转换和C语言风格的强制转换都有安全隐患。
2、用于类层次结构中基类(父类)和派生类(子类)之间指针或引用的转换。**注意:**进行上行转换(把派生类的指针或引用转换成基类表示)是安全的;进行下行转换(把基类指针或引用转换成派生类表示)时,由于没有动态类型检查,所以是不安全的。
3、用于基本数据类型之间的转换,如把int转换成char,把int转换成enum。这种转换的安全性需要开发者来维护。
4、static_cast不能转换掉原有类型的const、volatile、或者 __unaligned属性。(前两种可以使用const_cast 来去除)
5、在c++ primer 中说道:c++ 的任何的隐式转换都是使用 static_cast 来实现。

/* 常规的使用方法 */
float f_pi=3.141592f
int   i_pi=static_cast<int>(f_pi); /// i_pi 的值为 3

/* class 的上下行转换 */
class Base{
    // something
};
class Sub:public Base{
    // something
}

//  上行 Sub -> Base
//编译通过,安全
Sub sub;
Base *base_ptr = static_cast<Base*>(&sub);  

//  下行 Base -> Sub
//编译通过,不安全
Base base;
Sub *sub_ptr = static_cast<Sub*>(&base);    

2.3 dynamic_cast
dynamic_cast强制转换,应该是这四种中最特殊的一个,因为他涉及到面向对象的多态性程序运行时的状态,也与编译器的属性设置有关.所以不能完全使用C语言的强制转换替代,它也是最常有用的,最不可缺少的一种强制转换.
2.4 reinterpret_cast
reinterpret_cast是强制类型转换符用来处理无关类型转换的,通常为操作数的位模式提供较低层次的重新解释!但是他仅仅是重新解释了给出的对象的比特模型,并没有进行二进制的转换!
他是用在任意的指针之间的转换,引用之间的转换,指针和足够大的int型之间的转换,整数到指针的转换。

14 构造函数和析构函数的作用是什么?什么时候需要自己定义构造函数和析构函数?

构造函数的作用:用于新建对象的初始化工作。
析构函数的作用:用于在撤销对象前,完成一些清理工作,比如:释放内存等。
每当创建对象时,需要添加初始化代码时,则需要定义自己的构造函数;而对象撤销时,需要自己添加清理工作的代码时,则需要定义自己的析构函数。

15 C++中指针和引用的区别

1.指针和引用的定义和性质区别
1) 指针:指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。如:`

int a=1;int *p=&a;
int a=1;int &b=a;

上面定义了一个整形变量和一个指针变量p,该指针变量指向a的存储单元,即p的值是a存储单元的地址。
而下面2句定义了一个整形变量a和这个整形a的引用b,事实上a和b是同一个东西,在内存占有同一个存储单元。
2) 引用不可以为空,当被创建的时候,必须初始化,而指针可以是空值,可以在任何时候被初始化。
3) 可以有const指针,但是没有const引用
4) 指针可以有多级,但是引用只能是一级(int p;合法 而 int &&a是不合法的)
5) 指针的值可以为空,但是引用的值不能为NULL,并且
引用在定义的时候必须初始化**;
6) 指针的值在初始化后可以改变,即指向其它的存储单元,而引用在进行初始化后就不会再改变了
7) sizeof引用”得到的是所指向的变量(对象)的大小,而”sizeof指针”得到的是指针本身的大小
8) 指针和引用的自增(++)运算意义不一样
9) 如果返回动态内存分配的对象或者内存,必须使用指针,引用可能引起内存泄漏;

2.指针和引用作为函数参数进行传递时的区别
1、指针传递参数,可以实现对实参进行改变的目的,是因为传递过来的是实参的地址,因此使用*a实际上是取存储实参的内存单元里的数据,即是对实参进行改变,因此可以达到目的。

2、 将引用作为函数的参数进行传递。 讲引用作为函数参数进行传递时,实质上传递的是实参本身,即传递进来的不是实参的一个拷贝,因此对形参的修改其实是对实参的修改,所以在用引用进行参数传递时,不仅节约时间,而且可以节约空间。

16 static关键字的作用

https://blog.csdn.net/wordwarwordwar/article/details/84931897
1、修饰局部变量
static修饰局部变量时,使得被修饰的变量成为静态变量,存储在静态区。存储在静态区的数据生命周期与程序相同,在main函数之前初始化,在程序退出时销毁。(无论是局部静态还是全局静态)
2 修饰全局变量
全局变量本来就存储在静态区,因此static并不能改变其存储位置。但是,static限制了其链接属性。被static修饰的全局变量只能被该包含该定义的文件访问(即改变了作用域)。
3 、修饰函数
static修饰函数使得函数只能在包含该函数定义的文件中被调用。对于静态函数,声明和定义需要放在同一个文件夹中。
4、修饰成员变量
用static修饰类的数据成员使其成为类的全局变量会被类的所有对象共享,包括派生类的对象所有的对象都只维持同一个实例
5、修饰成员函数
用static修饰成员函数,使这个类只存在这一份函数,所有对象共享该函数,不含this指针,因而只能访问类的static成员变量。

17 null与nullptr区别

C的NULL
在C语言中,我们使用NULL表示空指针,也就是我们可以写如下代码:

int *i = NULL;
foo_t *f = NULL;

实际上在C语言中,NULL通常被定义为如下:

#define NULL ((void *)0)

也就是说NULL实际上是一个void *的指针,然后吧void *指针赋值给int *和foo_t *的指针的时候,隐式转换成相应的类型。而如果换做一个C++编译器来编译的话是要出错的,因为C++是强类型的void *是不能隐式转换成其他指针类型的

C++程序中的NULL
因为C++是强类型语言void*是不能隐式转换成其他类型的指针的,所以实际上编译器提供的头文件做了相应的处理:

#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif

可见,在C++中,**NULL实际上是0.**因为C++中不能把void*类型的指针隐式转换成其他类型的指针,所以为了结果空指针的表示问题,C++引入了0来表示空指针,这样就有了上述代码中的NULL宏定义。

但是实际上,用NULL代替0表示空指针在函数重载时会出现问题,程序执行的结果会与我们的想法不同,举例如下:

#include 
using namespace std;
 
void func(void* i)
{
	cout << "func1" << endl;
}
 
void func(int i)
{
	cout << "func2" << endl;
}
 
void main(int argc,char* argv[])
{
	func(NULL);
	func(nullptr);
	getchar();
}

在这里插入图片描述
我们对函数func进行可重载,参数分别是void*类型和int类型,但是运行结果却与我们使用NULL的初衷是相违背的,因为我们本来是想用NULL来代替空指针,但是在将NULL输入到函数中时,它却选择了int形参这个函数版本,所以是有问题的, 这就是用NULL代替空指针在C++程序中的二义性。

C++中的nullptr
解决NULL代指空指针存在的二义性问题,在C++11版本(2011年发布)中特意引入了nullptr这一新的关键字来代指空指针

总结:
NULL在C++中就是0, 这是因为在C++中void* 类型是不允许隐式转换成其他类型的,所以之前C++中用0来代表空指针,但是在重载整形的情况下,会出现上述的问题。所以,C++11加入了nullptr,可以保证在任何情况下都代表空指针,而不会出现上述的情况,因此,建议以后还是都用nullptr替代NULL吧,而NULL就当做0使用。

18 C/C++中volatile关键字详解

volatile 关键字是一种类型修饰符,用它声明的类型变量表示可以被某些编译器未知的因素更改,比如:操作系统、硬件或者其它线程等。
遇到这个关键字声明的变量,编译器对访问该变量的代码就不再进行优化,从而可以提供对特殊地址的稳定访问
声明时语法:int volatile vInt; 当要求使用 volatile 声明的变量的值的时候,系统总是重新从它所在的内存读取数据,即使它前面的指令刚刚从该处读取过数据。而且读取的数据立刻被保存。
例如:


1	volatile int i=10;
2	int a = i;
3	...
4	// 其他代码,并未明确告诉编译器,对 i 进行过操作
5	int b = i;

volatile 指出 i 是随时可能发生变化的,每次使用它的时候必须从 i的地址中读取,因而编译器生成的汇编代码会重新从i的地址读取数据放在 b 中。而优化做法是,由于编译器发现两次从 i读数据的代码之间的代码没有对 i 进行过操作,它会自动把上次读的数据放在 b 中。而不是重新从 i 里面读。

总结:使用volatile修饰的变量每一次都是从变量的地址中进行读取数据,不会进行优化。

19 C++11线程中的几种锁

线程之间的锁有:互斥锁、条件锁、自旋锁、读写锁、递归锁
互斥锁(Mutex)
互斥锁用于控制多个线程对他们之间共享资源互斥访问的一个信号量。也就是说是为了避免多个线程在某一时刻同时操作一个共享资源。例如线程池中的有多个空闲线程和一个任务队列。任何是一个线程都要使用互斥锁互斥访问任务队列,以避免多个线程同时访问任务队列以发生错乱。
某一时刻,只有一个线程可以获取互斥锁,在释放互斥锁之前其他线程都不能获取该互斥锁。如果其他线程想要获取这个互斥锁,那么这个线程只能以阻塞方式进行等待。

条件锁
条件锁就是所谓的条件变量,某一个线程因为某个条件为满足时可以使用条件变量使改程序处于阻塞状态。一旦条件满足以“信号量”的方式唤醒一个因为该条件而被阻塞的线程。最为常见就是在线程池中,起初没有任务时任务队列为空,此时线程池中的线程因为“任务队列为空”这个条件处于阻塞状态。一旦有任务进来,就会以信号量的方式唤醒一个线程来处理这个任务。

自旋锁
下面通过比较互斥锁和自旋锁原理的不同,这对于真正理解自旋锁有很大帮助。

假设我们有一个两个处理器core1和core2计算机,现在在这台计算机上运行的程序中有两个线程:T1和T2分别在处理器core1和core2上运行,两个线程之间共享着一个资源。

首先我们说明互斥锁的工作原理,互斥锁是是一种sleep-waiting的锁。假设线程T1获取互斥锁并且正在core1上运行时,此时线程T2也想要获取互斥锁(pthread_mutex_lock),但是由于T1正在使用互斥锁使得T2被阻塞。当T2处于阻塞状态时,T2被放入到等待队列中去,处理器core2会去处理其他任务而不必一直等待(忙等)。也就是说处理器不会因为线程阻塞而空闲着,它去处理其他事务去了。
自旋锁就不同了,自旋锁是一种busy-waiting的锁。也就是说,如果T1正在使用自旋锁,而T2也去申请这个自旋锁,此时T2肯定得不到这个自旋锁。与互斥锁相反的是,此时运行T2的处理器core2会一直不断地循环检查锁是否可用(自旋锁请求),直到获取到这个自旋锁为止。
从“自旋锁”的名字也可以看出来,如果一个线程想要获取一个被使用的自旋锁,那么它会一致占用CPU请求这个自旋锁使得CPU不能去做其他的事情,直到获取这个锁为止,这就是“自旋”的含义。

读写锁
说到读写锁我们可以借助于“读者-写者”问题进行理解。首先我们简单说下“读者-写者”问题。

计算机中某些数据被多个进程共享,对数据库的操作有两种:一种是读操作,就是从数据库中读取数据不会修改数据库中内容;另一种就是写操作,写操作会修改数据库中存放的数据。**因此可以得到我们允许在数据库上同时执行多个“读”操作,但是某一时刻只能在数据库上有一个“写”操作来更新数据。**这就是一个简单的读者-写者模型。

20 STL中map和unordered_map 区别、

1、需要引入的头文件不同

map: #include < map >
unordered_map: #include < unordered_map >

2、内部实现机理不同
map: map内部实现了一个红黑树(红黑树是非严格平衡二叉搜索树,而AVL是严格平衡二叉搜索树),红黑树具有自动排序的功能,因此map内部的所有元素都是有序的,红黑树的每一个节点都代表着map的一个元素。因此,对于map进行的查找,删除,添加等一系列的操作都相当于是对红黑树进行的操作。map中的元素是按照二叉搜索树(又名二叉查找树、二叉排序树,特点就是左子树上所有节点的键值都小于根节点的键值,右子树所有节点的键值都大于根节点的键值)存储的,使用中序遍历可将键值按照从小到大遍历出来。
unordered_map: unordered_map内部实现了一个哈希表(也叫散列表,通过把关键码值映射到Hash表中一个位置来访问记录,查找的时间复杂度可达到O(1),其在海量数据处理中有着广泛应用)。因此,其元素的排列顺序是无序的
3、优缺点以及适用处
map:
1、优点:
1)有序性,这是map结构最大的优点,其元素的有序性在很多应用中都会简化很多的操作
2)红黑树,内部实现一个红黑书使得map的很多操作在lgn的时间复杂度下就可以实现,因此
效率非常的高

2、缺点: 空间占用率高,因为map内部实现了红黑树,虽然提高了运行效率,但是因为每一个节点都需要额外保存父节点、孩子节点和红/黑性质,使得每一个节点都占用大量的空间
3、适用处:对于那些有顺序要求的问题,用map会更高效一些

unordered_map:
1、优点: 因为内部实现了哈希表,因此其查找速度非常的快
2、缺点: 哈希表的建立比较耗费时间
3、适用处: 对于查找问题,unordered_map会更加高效一些,因此遇到查找问题,常会考虑一下用unordered_map

21 哈希表介绍一下,冲突解决

哈希表,也称散列表,从根本上来说,一个哈希表包含一个数组,通过特殊的关键码(也就是key)来访问数组中的元素。哈希表的主要思想通过一个哈希函数, 把关键码映射的位置去寻找存放值的地方 ,读取的时候也是直接通过关键码来找到位置并存进去。
1、常见的哈希算法
1) 直接定址法
取关键字或关键字的某个线性函数值为散列地址。
即 f(key) = key 或 f(key) = a*key + b,其中a和b为常数。

2) 除留余数法 最常用
取关键字被某个不大于散列表长度 m 的数 p 求余,得到的作为散列地址。
即 f(key) = key % p, p < m。这是最为常见的一种哈希算法。

3) 数字分析法
当关键字的位数大于地址的位数,对关键字的各位分布进行分析,选出分布均匀的任意几位作为散列地址。
仅适用于所有关键字都已知的情况下,根据实际应用确定要选取的部分,尽量避免发生冲突。

4) 平方取中法
先计算出关键字值的平方,然后取平方值中间几位作为散列地址。
随机分布的关键字,得到的散列地址也是随机分布的。

5) 随机数法
选择一个随机函数,把关键字的随机函数值作为它的哈希值。
通常当关键字的长度不等时用这种方法。

2、哈希冲突
一、开放地址法
开发地址法的做法是,当冲突发生时,使用某种探测算法在散列表中寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到。按照探测序列的方法,一般将开放地址法区分为线性探查法、二次探查法、双重散列法等。

二、链地址法。

22 面向对象,封装,继承,多态,介绍一下,这三者的意义分别是什么,这样做优势在哪?

1 、如何理解面向对象
面向对象可以说是一种对现实是事物的抽象,将一类事物抽象成一个类类里面包含了这类事物具有的公共部分,以及我们对这些部分的操作,也就是对应的数据和过程。
2 、封装
利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体数据保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外的接口使其与外部发生联系。用户无需关心对象内部的细节,但可以通过对象对外提供的接口来访问该对象

优点:
1、减少耦合:可以独立地开发、测试、优化、使用、理解和修改
2、减轻维护的负担:可以更容易被理解,并且在调试的时候可以不影响其他模块
3、有效地调节性能:可以通过剖析来确定哪些模块影响了系统的性能
4、提高软件的可重用性
5、降低了构建大型系统的风险:即使整个系统不可用,但是这些独立的模块却有可能是可用的

3、 继承
继承可以说是一种代码复用的手段,我们在一个现有类上想扩展出一些东西的时候,不需要再次重复编写上面的代码,而是采用一种继承的思想。在派生出的子类里添加一些我们想要的数据或方法,也可以理解为从一般到特殊的过程。

4、多态
多态简单理解为就是同一函数,在基类和派生类中表现出不同的效果。
多态分为编译时多态运行时多态
1、编译时多态主要指方法的重载
2、运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定

这里先说运行时多态,其实是指在继承体系中父类的一个接口(必须为虚函数),在子类中有多种不同的实现,父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。即通过父类指针或者引用可以访问到子类的接口(虚函数)看上去就像是一个相同的动作,会出现多种不同的结果
就可以理解为,调用接口时与类型无关(用父类的指针或者引用),与对象有关(父类指针或引用指向不同的对象,而调用到不同的接口)

多态是如何实现,主要还是虚表,只要类里面有虚函数,就会在静态区开辟一块空间来保存虚函数(属于整个类域),每个对象里面都有一个虚表指针,虚表是在编译的时候进行初始化的,虚表指针是在初始化列表中初始化。我们知道用父类的指针可以指向子类对象(发生切片行为),但是虚表是不变的,访问的虚函数就是子类的虚函数了。

23 C++的智能指针

1 普通指针(normal/raw/naked pointers)的问题?
如果不恰当处理指针就会带来许多问题,所以人们总是避免使用它。指针总是会扯上很多问题,例如指针所指向对象的生命周期,挂起引用(dangling references)以及内存泄露
如果一块内存被多个指针引用,但其中的一个指针释放其余的指针并不知道,这样的情况下,就发生了挂起引用。而内存泄露,就如你知道的一样,当从堆中申请了内存后不释放回去,这时就会发生内存泄露。

2 什么是智能指针?
智能指针是一个RAII(Resource Acquisition is initialization)类模型,用来动态的分配内存。它提供所有普通指针提供的接口,却很少发生异常。在构造中,它分配内存,当离开作用域时,它会自动释放已分配的内存。这样的话,程序员就从手动管理动态内存的繁杂任务中解放出来了。

智能指针就是将基本类型指针封装为类对象指针(这个类肯定是个模板,以适应不同基本类型的需求),并在析构函数里编写delete语句删除指针指向的内存空间。

3 auto_ptr
让我们来见识一下auto_ptr如何解决上述问题的吧。

class Test
{
    public: 
    Test(int a = 0 ) : m_a(a) { }
    ~Test( )
    { 
       cout << "Calling destructor" << endl; 
    }
    public: int m_a;
};
void main( )
{ 
    std::auto_ptr<Test> p( new Test(5) ); 
    cout << p->m_a << endl;
}  

上述代码会智能地释放与指针绑定的内存作用的过程是这样的:我们申请了一块内存来放Test对象,并且把他绑定auto_ptr p上。所以当p离开作用域时,它所指向的内存块也会被自动释放。
但是,他也存在着很多问题:
问题1:auto_ptr不能指向一组对象,就是说它不能和操作符new[]一起使用。
问题2: auto_ptr不能和标准容器(vector,list,map…)一起使用。
因为auto_ptr容易产生错误,所以它也将被废弃了。C++11提供了一组新的智能指针,每一个都各有用武之地。

4 shared_ptr
第一种智能指针是shared_ptr,它有一个叫做共享所有权(sharedownership)的概念shared_ptr的目标非常简单:多个指针可以同时指向一个对象,当最后一个shared_ptr离开作用域时,内存才会自动释放。

shared_ptr完善了前两种的不足,既不会直接剥夺原对象对内存的控制权,也允许进行拷贝构造和赋值,这都源自于他引入了一个新的标志—引用计数。引用计数记录着有多少块

shared_ptr默认调用delete释放关联的资源。

shared_ptr的原理:是通过引用计数的方式来实现多个shared_ptr对象之间共享资源。

  1. shared_ptr在其内部,给每个资源都维护了着一份计数,用来记录该份资源被几个对象共享。
  2. 在对象被销毁时(也就是析构函数调用),就说明自己不使用该资源了,对象的引用计数减一。
  3. 如果引用计数是0,就说明自己是最后一个使用该资源的对象,必须释放该资源;
  4. 如果不是0,就说明除了自己还有其他对象在使用该份资源,不能释放该资源,否则其他对象就成野指针了。
void main( )
{
 shared_ptr<int> sptr1( new int );
}

5 Weak_Ptr
weak_ptr 拥有共享语义(sharing semantics)和不包含语义(not owning semantics)。这意味着,weak_ptr可以共享shared_ptr持有的资源。所以可以从一个包含资源的shared_ptr创建weak_ptr
weak_ptr不支持普通指针包含的*,->操作。它并不包含资源所以也不允许程序员操作资源

void main( )
{
 shared_ptr<Test> sptr( new Test );
 weak_ptr<Test> wptr( sptr );
 weak_ptr<Test> wptr1 = wptr;
}

6 unique_ptr
unique_ptr也是对auto_ptr的替换。unique_ptr遵循着独占语义。在任何时间点资源只能唯一地被一个unique_ptr占有。当unique_ptr离开作用域,所包含的资源被释放。如果资源被其它资源重写了,之前拥有的资源将被释放。所以它保证了他所关联的资源总是能被释放

unique_ptr<int> uptr( new int );

7 使用哪一个?
完全取决于你想要如何拥有一个资源,如果需要共享资源使用shared_ptr,如果独占使用资源就使用unique_ptr.

23* shared_ptr引发的线程安全问题:

在多进程程序下,多个进程都去访问shared_ptr管理的空间,如果线程是并行的,那么引用计数会可能发生错误!如图:
寒武纪软件开发面试题C++_第1张图片

23 ** shared_ptr所导致的循环引用的问题

当前的shared_ptr已经能解决绝大多数的问题了,但还是有一点点的瑕疵。就是在循环引用的时候还会造成内存泄漏

struct ListNode
{
	int _data;
	shared_ptr<ListNode> _prev;
	shared_ptr<ListNode> _next;
 
	ListNode(int x)
		:_data(x)
		, _prev(NULL)
		,_next(NULL)
	{}
	~ListNode()
	{
		cout << "~ListNode" << endl;
	}
};
int main()
{
	shared_ptr<ListNode> cur(new ListNode(1));
	shared_ptr<ListNode> next(new ListNode(2));
	cur->_next = next;
	next->_prev = cur;
	cout << "cur" << "     " << cur.use_count() << endl;
	cout << "next" << "     " << next.use_count() << endl;
	return 0;
}


寒武纪软件开发面试题C++_第2张图片
C++库为了解决这个问题,专门定义了一个叫做weak_ptr的东西,专门用于辅助shared_ptr来解决引用计数的问题。那他是怎么解决这么问题的呢?当shared_ptr内部要监视其他的shared_ptr对象时,类型就采用weak_ptr。这种weak_ptr在指向被监视的shared_ptr后,并不会使被监视的引用计数增加,且当被监视的对象析构后就自动失效。

然后它就什么都不管光是个删 , 也就是这里的cur和next在析构的时候 , 不用引用计数减一 , 直接删除结点就好。这样也就间接地解决了循环引用的问题,当然week_ptr指针的功能不是只有这一个。但是现在我们只要知道它可以解决循环引用就好。

补充:智能指针是线程安全的吗?
对于unique_ptr,由于只是在当前代码块范围内有效。所以不涉及线程安全的问题。

对于shared_ptr,多个对象要同时共用一个引用计数变量,所以会存在线程安全的问题,但是标准库实现的时候考虑到了这一点,使用了基于原子操作(CAS)的方式来保证shared_ptr能够高效,原子的操作引用计数。

最后总结一下我们学过的这几种智能指针

  1. 不要使用auto_ptr,因为他的缺陷导致我们拷贝构造/赋值的时候有很大的麻烦。
  2. 在不需要拷贝构造/赋值的时候,可以使用unique_ptr。
  3. 有拷贝构造/赋值的情况,推荐使用shared_ptr.
  4. 类内有访问其他shared_ptr对象时,指针类型设为weak_ptr,可以不改其他其他shared_ptr对象的引用计数。
  5. 代码中尽量不用delete关键字,因为我们的内存的管理与释放全权交给对象处理。

24 【C++】浅拷贝和深拷贝

【浅拷贝】增加了一个指针指向原来已经存在的内存。

【深拷贝】增加了一个指针,并新开辟了一块空间 ,让 指针指向这块新开辟的空间

【浅拷贝】 在多个对象指向一块空间的时候,释放一个空间会导致其他对象所使用的空间也被释放了,再次释放便会出现错误

如果使用默认的复制(拷贝)构造函数,那就对有指针成员变量的对象会有问题,因为会默认的复制(拷贝)构造函数会导致两个对象的指针成员变量指向同一个的空间

所以需要对复制(拷贝)构造函数重载,并实现深拷贝的方式

25 * STL的分类

STL的代码从广义上讲分为三类:algorithm(算法)container(容器)iterator(迭代器)。几乎所有的代码都采用了模板类模板函数的方式,这相比于传统的由函数和类组成的库来说提供了更好的代码重用机会。

在C++标准中,STL被组织为下面13个头文件:

25 list和vector有什么区别?

  • vector和数组类似,它拥有一段连续的内存空间,并且起始地址不变,因此它能非常好的支持随机存取(即使用[]操作符访问其中的元素),但由于它的内存空间是连续的,所以在中间进行插入和删除会造成内存块的拷贝(复杂度是O(n)),另外,当该数组后的内存空间不够时,需要重新申请一块足够大的内存并进行内存的拷贝。这些都大大影响了vector的效率。
  • list是由数据结构中的双向链表实现的,因此它的内存空间可以是不连续的。因此只能通过指针来进行数据的访问,这个特点使得它的随机存取变的非常没有效率,需要遍历中间的元素,搜索复杂度O(n),因此它没有提供[]操作符的重载。但由于链表的特点,它可以以很好的效率支持任意地方的删除和插入。

26 构造函数和析构函数能否为虚函数

C++:构造函数和析构函数能否为虚函数?

简单回答是:构造函数不能为虚函数,而析构函数可以且常常是虚函数

(1) 构造函数不能为虚函数
这就要涉及到C++对象的构造问题了,C++对象在三个地方构建:(1)函数堆栈;(2)自由存储区,或称之为堆;(3)静态存储区。无论在那里构建,其过程都是两步:首先,分配一块内存;其次,调用构造函数。好,问题来了,如果构造函数是虚函数,那么就需要通过vtable 来调用,但此时面对一块 raw memeory,到哪里去找 vtable 呢?毕竟,vtable 是在构造函数中才初始化的啊,而不是在其之前。因此构造函数不能为虚函数。

(2)析构函数可以是虚函数,且常常如此
这个就好理解了,因为此时 vtable 已经初始化了;况且我们通常通过基类的指针来销毁对象,如果析构函数不为虚的话,就不能正确识别对象类型,从而不能正确销毁对象。

27 C++ 内存管理

寒武纪软件开发面试题C++_第3张图片

静态区域和动态区域两个部分,静态区域主要用于存储程序中的代码部分、常量、全局的变量以及静态变量(全局+局部),而动态区域主要是系统或者程序员进行动态进行的分配的内存,是在程序运行中进行分配的。

1 静态区域

代码段(text segment): 包括只读存储区以及文本区,其中只读存储区存储的是字符串常量,文本区存储的是机器代码,比如一些可执行指令。

数据段(data segment): 用于存放程序中 已经初始化 的全局变量和静态变量

BSS段: 存储 未初始化 的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量(对于未初始化的全局变量和静态变量,程序运行main之前时会统一清零),即未初始化的全局变量编译器会初始化为0。

2 动态区域

堆(heap): 堆得大小并不固定,可以动态的扩张或者缩减。进行分配时是由malloc()以及new()这一类实时内存分配函数来实现的。刚开始,当进程未调用malloc()以及new()这一类实时内存分配函数时是没有堆段的,在进行调用这些实时分配函数之后分配一个堆段,并在程序运行过程中,可以动态的增加堆得大小,是由低地址向高地址增长的。

栈(stack): 用来存储函数调用时的临时信息,比如函数调用所传递的参数、函数的返回地址、函数的局部变量等等。在程序运行时,是由编译器在需要的时候进行分配,在不需要的时候自动清除。栈内存的申请和释放都按照先进后出(LIFO)。

C/C++内存布局如下图所示:
寒武纪软件开发面试题C++_第4张图片

3 区分堆和栈

如何进行区分堆和栈,我们通过一个小例子来进行解释:

void f(){
  int* p=new int[5];
  .......
}

上面的代码中就包括了栈和堆,new 语句显示的是分配了一块堆内存,对于指针变量p则是指栈内存中存放了一个指向堆内存的指针p。在程序中会先确定在堆中分配内存的大小,然后调用operator new进行分配内存,然后返回内存的首地址,放进栈中。同时我们还应该回收所分配的内存,使用delete []p,是为了告诉编译器,我们删除的是一个数组。

栈内存:程序 自动向操作系统申请分配以及回收,速度快,使用方便,但程序员无法控制。若分配失败,则提示栈溢出错误。注意,const局部变量也储存在栈区内,栈区向地址减小的方向增长。

堆内存: 程序员 向操作系统申请一块内存,当系统收到程序的申请时,会遍历一个记录空闲内存地址的链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。分配的速度较慢,地址不连续,容易碎片化。此外,由程序员申请,同时也必须由程序员负责销毁,否则则导致内存泄露。

关于堆和栈区别的比喻:

使用栈就象我们去饭馆里吃饭,只管点菜(发出申请)、付钱、和吃(使用),吃饱了就走,不必理会切菜、洗菜等准备工作和洗碗、刷锅等扫尾工作,他的好处是快捷,但是自由度小。

使用堆就象是自己动手做喜欢吃的菜肴,比较麻烦,但是比较符合自己的口味,而且自由度大。

堆和栈的区别

1. 管理方式不同:

  1. 对于来讲,是由编译器自动管理,无需我们手工控制;
  2. 对于来说,申请和释放工作由程序员控制,容易产生memory leak。

2. 空间大小不同:

  1. 对于来讲,一般都是有一定的空间大小的,一般默认的栈空间大小为1M,同时,我们是可以进行修改的;
  2. 对于来说,在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。

3. 能否产生碎片不同:

  1. 对于来讲,不会有碎片,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出,在他弹出之前,在他上面的后进的栈内容已经被弹出(具体操作和数据结构中的栈操作是一致的);
  2. 对于来说,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。

4. 生长方向不同:

  1. 对于来讲,生长方向是向下的,是向着内存地址减小的方向增长;(参考C/C++内存布局图)
  2. 对于来说,生长方向是向上的,也就是向着内存地址增加的方向。

5. 分配方式不同:

  1. 对于来讲,栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。;(参考C/C++内存布局图)
  2. 对于来说,堆都是动态分配的,没有静态分配的堆。

6. 分配效率不同:

  1. 对于来讲,栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。
  2. 对于来说,堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

从这里我们可以看到,堆和栈相比,由于大量new/delete的使用,容易造成大量的内存碎片;由于没有专门的系统支持,效率很低;由于可能引发用户态和核心态的切换,内存的申请,代价变得更加昂贵。所以栈在程序中是应用最广泛的,就算是函数的调用也利用栈去完成,函数调用过程中的参数,返回地址,EBP和局部变量都采用栈的方式存放。所以,我们推荐大家尽量用栈,而不是用堆。

28 C++中的静态多态和动态多态

C++支持多种形式的多态,从表现的形式来看,有虚函数、模板、重载等,从绑定时间来看,可以分成静态多态和动态多态,也称为编译期多态和运行期多态

什么是动态多态?
动态多态的设计思想:对于相关的对象类型,确定它们之间的一个共同功能集,然后在基类中,把这些共同的功能声明为多个公共的虚函数接口。各个子类重写这些虚函数,以完成具体的功能。客户端的代码(操作函数)通过指向基类的引用或指针来操作这些对象,对虚函数的调用会自动绑定到实际提供的子类对象上去。
由于有了
虚函数
,因此动态多态是在运行时完成的,也可以叫做运行期多态,这造就了动态多态机制在处理异质对象集合时的强大威力。

1 namespace DynamicPoly
 2 {
 3     class Geometry
 4     {
 5     public:
 6         virtual void Draw()const = 0;
 7     };
 8 
 9     class Line : public Geometry
10     {
11     public:
12         virtual void Draw()const{    std::cout << "Line Draw()\n";    }
13     };
14 
15     class Circle : public Geometry
16     {
17     public:
18         virtual void Draw()const{    std::cout << "Circle Draw()\n";    }
19     };
20 
21     class Rectangle : public Geometry
22     {
23     public:
24         virtual void Draw()const{    std::cout << "Rectangle Draw()\n";    }
25     };
26 
27     void DrawGeometry(const Geometry *geo)
28     {
29         geo->Draw();
30     }
31 
32     //动态多态最吸引人之处在于处理异质对象集合的能力
33     void DrawGeometry(std::vector<DynamicPoly::Geometry*> vecGeo)
34     {
35         const size_t size = vecGeo.size();
36         for(size_t i = 0; i < size; ++i)
37             vecGeo[i]->Draw();
38     }
39 }
40 
41 void test_dynamic_polymorphism()
42 {
43     DynamicPoly::Line line;
44     DynamicPoly::Circle circle;
45     DynamicPoly::Rectangle rect;
46     DynamicPoly::DrawGeometry(&circle);
47 
48     std::vector<DynamicPoly::Geometry*> vec;
49     vec.push_back(&line);
50     vec.push_back(&circle);
51     vec.push_back(&rect);
52     DynamicPoly::DrawGeometry(vec);
53 }

动态多态本质上就是面向对象设计中的继承、多态的概念。动态多态中的接口是显式接口(虚函数)!!!

什么是静态多态?
静态多态的设计思想:对于相关的对象类型,直接实现它们各自的定义不需要共有基类,甚至可以没有任何关系。只需要各个具体类的实现中要求相同的接口声明这里的接口称之为隐式接口。客户端把操作这些对象的函数定义为模板,当需要操作什么类型的对象时,直接对模板指定该类型实参即可(或通过实参演绎获得)。
相对于面向对象编程中,以显式接口运行期多态(虚函数)实现动态多态,在模板编程及泛型编程中,是以隐式接口和编译器多态来实现静态多态

1 namespace StaticPoly
 2 {
 3     class Line
 4     {
 5     public:
 6         void Draw()const{    std::cout << "Line Draw()\n";    }
 7     };
 8 
 9     class Circle
10     {
11     public:
12         void Draw(const char* name=NULL)const{    std::cout << "Circle Draw()\n";    }
13     };
14 
15     class Rectangle
16     {
17     public:
18         void Draw(int i = 0)const{    std::cout << "Rectangle Draw()\n";    }
19     };
20 
21     template<typename Geometry>
22     void DrawGeometry(const Geometry& geo)
23     {
24         geo.Draw();
25     }
26 
27     template<typename Geometry>
28     void DrawGeometry(std::vector<Geometry> vecGeo)
29     {
30         const size_t size = vecGeo.size();
31         for(size_t i = 0; i < size; ++i)
32             vecGeo[i].Draw();
33     }
34 }
35 
36 void test_static_polymorphism()
37 {
38     StaticPoly::Line line;
39     StaticPoly::Circle circle;
40     StaticPoly::Rectangle rect;
41     StaticPoly::DrawGeometry(circle);
42 
43     std::vector<StaticPoly::Line> vecLines;
44     StaticPoly::Line line2;
45     StaticPoly::Line line3;
46     vecLines.push_back(line);
47     vecLines.push_back(line2);
48     vecLines.push_back(line3);
49     //vecLines.push_back(&circle); //编译错误,已不再能够处理异质对象
50     //vecLines.push_back(&rect);    //编译错误,已不再能够处理异质对象
51     StaticPoly::DrawGeometry(vecLines);
52 
53     std::vector<StaticPoly::Circle> vecCircles;
54     vecCircles.push_back(circle);
55     StaticPoly::DrawGeometry(circle);
56 }

动态多态和静态多态的比较
静态多态
优点:

  1. 由于静多态是在编译期完成的,因此效率较高,编译器也可以进行优化;
  2. 很强的适配性和松耦合性,比如可以通过偏特化、全特化来处理特殊类型;
  3. 最重要一点是静态多态通过模板编程为C++带来了泛型设计的概念,比如强大的STL库。

缺点:

  1. 由于是模板来实现静态多态,因此模板的不足也就是静多态的劣势,比如调试困难、编译耗时、代码膨胀、编译器支持的兼容性
  2. 不能够处理异质对象集合

动态多态
优点:

  1. OO设计,对是客观世界的直觉认识;
  2. 实现与接口分离,可复用
  3. 处理同一继承体系下异质对象集合的强大威力

缺点:

  1. 运行期绑定,导致一定程度的运行时开销;
  2. 编译器无法对虚函数进行优化
  3. 笨重的类继承体系,对接口的修改影响整个类层次;

不同点:

  • 本质不同,静态多态在编译期决定,由模板具现完成,而动态多态在运行期决定,由继承、虚函数实现;
  • 动态多态中接口是显式的,以函数签名为中心,多态通过虚函数在运行期实现,静态多台中接口是隐式的,以有效表达式为中心,多态通过模板具现在编译期完成

相同点:

  • 都能够实现多态性,静态多态/编译期多态、动态多态/运行期多态;
  • 都能够使接口和实现相分离,一个是模板定义接口,类型参数定义实现,一个是基类虚函数定义接口,继承类负责实现;

29 malloc 底层实现与原理

结论:

  1. 当开辟的空间小于 128K 时,调用 brk()函数malloc 的底层实现是系统调用函数 brk() (free()来释放),其主要移动指针(_enddata(此时的 _enddata 指的是 Linux 地址空间中堆段的末尾地址,不是数据段的末尾地址)
  2. 当开辟的空间大于 128K 时mmap()系统调用函数来在虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空间来开辟。

具体内容:
当一个进程发生缺页中断的时候,进程会陷入核心态,执行以下操作:

  1. 检查要访问的虚拟地址是否合法
  2. 查找/分配一个物理页
  3. 填充物理页内容(读取磁盘,或者直接置0,或者什么都不做)
  4. 建立映射关系(虚拟地址到物理地址的映射关系)
  5. 重复执行发生缺页中断的那条指令

内存分配的原理:
从操作系统角度看,进程分配内存有两种方式,分别由两个系统调用完成brk 和 mmap (不考虑共享内存)

  1. brk 是将数据段(.data)的最高地址指针 _edata 往高地址推
  2. mmap 是在进程的虚拟地址空间中(堆和栈中间,称为“文件映射区域”的地方)找一块空闲的虚拟内存。

这两种方式分配的都是虚拟内存,没有分配物理内存。在第一次访问已分配的虚拟地址空间的时候,发生缺页中断,操作系统负责分配物理内存,然后建立虚拟内存和物理内存之间的映射关系。

具体分配过程:

情况一:malloc 小于 128K 的内存,使用 brk 分配

将_edata往高地址推(只分配虚拟空间,不对应物理内存(因此没有初始化),第一次读/写数据时,引起内核缺页中断,内核才分配对应的物理内存,然后虚拟地址空间建立映射关系),如下图:
寒武纪软件开发面试题C++_第5张图片
情况二:malloc 大于 128K 的内存,使用 mmap 分配(munmap 释放)

寒武纪软件开发面试题C++_第6张图片

寒武纪软件开发面试题C++_第7张图片

双向链表的插入

寒武纪软件开发面试题C++_第8张图片

你可能感兴趣的:(面经)