C/C++面试题

文章目录

  • 数据类型说明
  • C和C++的区别
  • 封装、继承、多态
  • 虚函数的作用及其实现原理
  • 深拷贝和浅拷贝(值拷贝和位拷贝)
  • 虚函数、纯虚函数怎么实现
  • 为什么要有纯虚函数
  • 纯虚函数
  • 为什么要有虚析构函数
  • 构造函数能不能是虚函数
  • C++里面构造函数能有返回值吗?
  • 构造函数和析构函数能被继承吗?
  • C++中Overload、Overwrite及Override的区别
  • 一个空的class类里有什么
  • C++中一个空类的大小为什么是1?
  • 一个结构体中有一个int,一个char,一个static int,问这个结构体占多少内存?(涉及到内存对齐机制)
  • 结构体与联合体的区别
  • 函数与宏的差别
  • 宏函数和inline函数的异同点
  • define 和 typedef 区别
  • 标准C++中的include "" 与<>的区别
  • C++的内存管理机制
  • C语言中的malloc/free和C++中的new/delete的区别和联系
  • 迭代和递归区别
  • 不可操作的操作符
  • C++关键字mutable作用
  • 引用与指针有什么区别?
  • 什么是黑盒测试和白盒测试?
  • 你知道的类模版有哪些
  • new可以搭配free吗,为什么?
  • 怎么查看内存泄漏
  • 什么是内存溢出
  • 内存溢出的解决方案
  • 函数指针与指针函数分别是什么
  • C++11新特性了解吗
  • 接口和抽象类的区别
  • struct 和 class的区别
  • 预编译在做些什么事情?
  • 可执行文件编译过程
  • 动态库和静态库?
  • 堆和栈的区别,以及为什么栈效率高
  • 函数参数压栈方式为什么是从右到左的?
  • C++中的智能指针
  • 基类里private成员函数可以声明为虚函数吗?
  • 函数A调用函数B的时候,有什么需要压栈?
  • 数组和指针区别?数组和链表呢?双向链表和单向链表?
  • vector底层实现?
  • vector与list的区别?

数据类型说明

C/C++面试题_第1张图片

C和C++的区别

C是一个结构化语言。C程序的设计首要考虑的是如何通过一个过程,对输入进行运算处理得到输出。C++在C的基础上增添类,首要考虑的是如何构造一个对象模型,所以C++是面向对象的语言。

封装、继承、多态

封装
封装是把过程和数据包围起来,对数据的访问只能通过已定义的函数或属性。把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。封装可以隐藏实现细节,使得代码模块化。

多态
多态指同一个实体同时具有多种形式。在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。C++中,实现多态有以下方法:虚函数,抽象类,覆盖,模板,条件是要有重写,要有继承,父类指向子类。
继承
  如果一个类别A“继承自”另一个类别B,就把这个A称为“B的子类”,而把B称为“A的父类”。继承可以使得子类具有父类的各种属性和方法,而不需要再次编写相同的代码。在令子类别继承父类别的同时,可以重新定义某些属性,并重写某些方法,即覆盖父类别的原有属性和方法,使其获得与父类别不同的功能,这就是多态。为子类别追加新的属性和方法也是常见的做法。
  
继承概念的实现方式有三类:实现继承、接口继承和可视继承。

实现继承是指使用基类的属性和方法而无需额外编码的能力;
接口继承是指仅使用属性和方法的名称、但是子类必须提供实现的能力;
可视继承是指子窗体(类)使用基窗体(类)的外观和实现代码的能力。

虚函数的作用及其实现原理

C/C++面试题_第2张图片

虚函数的作用:
虚函数实现了多态的机制。基类的函数前加上virtual关键字定义了虚函数,子类可以重写该函数,当子类重新定义了父类的虚函数后,父类指针根据赋给它的不同的子类指针,动态地调用属于子类的该函数,且这样的函数调用是无法在编译器期间确认的,而是在运行期确认,叫做动态绑定。只有指定为虚函数的成员函数才能进行动态绑定,成员函数默认为非虚函数,非虚函数不进行动态绑定;必须通过基类类型的指针或引用进行函数的调用。
举例:
从形状类Shape可以派生出圆Circle、三角形Triangle、矩形Rectangle、正方形Square等。每个类都有一个成员函数Draw,表示在屏幕上画出自己的形状。使用者使用Shape*调用Draw,那么要使画出的形状都正确,必须根据对象类型来确定使用哪个Draw来画。将Draw在Shape中定义为虚函数,并在每个派生类中重新定义该函数,那么当程序运行时,系统自动调用合适的Draw函数版本。

深拷贝和浅拷贝(值拷贝和位拷贝)

深拷贝指拷贝时对象资源重新分配,两个对象的资源内存不同,释放一个对象资源不会影响另一个。浅拷贝指两个对象均指向同一内存空间,释放一个对象的资源,另一个对象的资源也没了,造成野指针。

虚函数、纯虚函数怎么实现

用virtual关键字申明的函数叫做虚函数,虚函数肯定是类的成员函数;
存在虚函数的类都有一个一维的虚函数表叫做虚表,类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的;
多态性是一个接口多种实现,是面向对象的核心,分为类的多态性和函数的多态性;
多态用虚函数来实现,结合动态绑定;
纯虚函数是虚函数再加上 = 0;
抽象类是指包括至少一个纯虚函数的类。纯虚函数:virtual void fun()=0;即抽象类!抽象基类不能定义对象。必须在子类实现这个函数,即先有名称,没有内容,在派生类实现内容。

为什么要有纯虚函数

为了方便使用多态特性,我们常常需要在基类中定义虚拟函数;
在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理;  
为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。

纯虚函数

纯虚函数没有函数体;
最后面的“=0”并不表示函数返回值为0,它只起形式上的作用,告诉编译系统“这是虚函数”;
这是一个声明语句,最后有分号。

为什么要有虚析构函数

C++中基类采用virtual虚析构函数是为了防止内存泄漏。具体地说,如果派生类中申请了内存空间,并在其析构函数中对这些内存空间进行释放。假设基类中采用的是非虚析构函数,当删除基类指针指向的派生类对象时就不会触发动态绑定,因而只会调用基类的析构函数,而不会调用派生类的析构函数。那么在这种情况下,派生类中申请的空间就得不到释放从而产生内存泄漏。所以,为了防止这种情况的发生,C++中基类的析构函数应采用virtual虚析构函数。

构造函数能不能是虚函数

不能。构造一个对象时,必须知道对象实际类型,而虚函数是在运行期间确定实际类型的。而在构造一个对象时,由于对象还未构造成功,编译器就无法知道对象的实际类型,是该类本身,还是派生类,还是其他。
虚函数的执行依赖于虚函数表,而虚函数表是在构造函数中进行初始化的,即初始化虚表指针(vptr),使得正确指向虚函数表。而在构造对象期间,虚函数表(vtable)还没有被初始化,将无法进行。

C++里面构造函数能有返回值吗?

构造函数没有返回值,他只是描述了类初始化的行为;但是new一个类实例是有返回值的,因为new返回的是类实例的指针。

构造函数和析构函数能被继承吗?

不能。不是所有的函数都能自动地从基类继承到派生类中的。构造函数和析构函数是用来处理对象的创建和析构的,它们只知道对在它们的特殊层次的对象做什么。
所以,在整个层次中的所有的构造函数和析构函数都必须被调用,也就是说,构造函数和析构函数不能被继承。子类的构造函数会显示的调用父类的构造函数或隐式的调用父类的默认的构造函数进行父类部分的初始化。
析构函数也一样。它们都是每个类都有的东西,如果能被继承,那就没有办法初始化了。

C++中Overload、Overwrite及Override的区别

Overload(重载):
在C++程序中,可以将语义、功能相似的几个函数用同一个名字表示,但参数或返回值不同(包括类型、顺序不同),即函数重载。
(1)相同的范围(在同一个类中);
(2)函数名字相同;
(3)参数不同;
(4)virtual 关键字可有可无。

Override(覆盖):
指派生类函数覆盖基类函数,特征是:
(1)不同的范围(分别位于派生类与基类);
(2)函数名字相同;
(3)参数相同;
(4)基类函数必须有virtual 关键字。

Overwrite(重写):
是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
(1)如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。
(2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。

一个空的class类里有什么

构造函数
拷贝构造函数
析构函数
赋值运算符重载
取地址操作符重载(this指针)
被const修饰的取地址操作符重载

C++中一个空类的大小为什么是1?

这就是实例化的原因(空类同样可以被实例化),每个实例在内存中都有一个独一无二的地址,为了达到这个目的,编译器往往会给一个空类隐含的加一个字节,这样空类在实例化后在内存得到了独一无二的地址,所以空类所占的内存大小是1个字节。

一个结构体中有一个int,一个char,一个static int,问这个结构体占多少内存?(涉及到内存对齐机制)

假定这个结构体所在的操作系统环境是64位,那么结构体中int占四字节,char内存对齐4字节,静态不计算,结构体一共占8字节内存。

特别注意的是: c结构体中不允许定义static变量; C++结构体中可以定义static变量,size of时不计算该变量, 但需注意初始化格式。
C/C++面试题_第3张图片
相同类型的数据放在连续的内存。

结构体与联合体的区别

结构体struct:
各成员各自拥有自己的内存,各自使用互不干涉,同时存在的,遵循内存对齐原则。一个struct变量的总长度等于所有成员的长度之和。
联合体union:
各成员共用一块内存空间,并且同时只有一个成员可以得到这块内存的使用权(对该内存的读写),各变量共用一个内存首地址。因而,联合体比结构体更节约内存。

函数与宏的差别

宏做的是简单的字符串替换,不会考虑数据类型;而函数是参数的传递,参数是有数据类型的。
宏的参数替换是不经计算而直接处理的,而函数调用是将实参的值传递给形参,既然说是值,自然是计算得来的。
宏占用的是编译的时间,而函数占用的是执行时的时间。
宏的参数是不占内存空间的,因为只做字符串的替换,形参作为函数的局部变量,是占用内存的.
函数的调用是需要付出一定的时空开销的,因为系统在调用函数时,要保留现场,然后转入被调用函数,执行完后返回主函数,再恢复现场,这些操作在宏中是没有的。

宏函数和inline函数的异同点

内联函数在编译时展开,而宏在预编译时展开;
在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换;
内联函数可以进行诸如类型安全检查、语句是否正确等编译功能,宏不具有这样的功能;
宏不是函数,而inline是函数;
宏在定义时要小心处理宏参数,一般用括号括起来,否则容易出现二义性。而内联函数不会出现二义性;
inline可以不展开,宏一定要展开。因为inline指示对编译器来说,只是一个建议,编译器可以选择忽略该建议,不对该函数进行展开;
宏定义在形式上类似于一个函数,但在使用它时,仅仅只是做预处理器符号表中的简单替换,因此它不能进行参数有效性的检测,也就不能享受C++编译器严格类型检查的好处,另外它的返回值也不能被强制转换为可转换的合适的类型,这样,它的使用就存在着一系列的隐患和局限性。

define 和 typedef 区别

原理不同
#define是C语言中定义的语法,是预处理指令,在预处理时进行简单而机械的字符串替换,不作正确性检查,只有在编译已被展开的源程序时才会发现可能的错误并报错。
typedef是关键字,在编译时处理,有类型检查功能。它在自己的作用域内给一个已经存在的类型一个别名,但不能在一个函数定义里面使用typedef。用typedef定义数组、指针、结构等类型会带来很大的方便,不仅使程序书写简单,也使意义明确,增强可读性。

功能不同
typedef用来定义类型的别名,起到类型易于记忆的功能。另一个功能是定义机器无关的类型。如定义一个REAL的浮点类型,在目标机器上它可以获得最高的精度:typedef long double REAL; 在不支持long double的机器上,看起来是这样的:typedef double REAL;在不支持double的机器上,是这样的:typedef float REAL
#define不只是可以为类型取别名,还可以定义常量、变量、编译开关等。

作用域不同
#define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而typedef有自己的作用域。

对指针的操作不同
#define INTPTR1 int*
typedef int* INTPTR2;

标准C++中的include “” 与<>的区别

#include<>直接从编译器自带的函数库中寻找文件
#include"“是先从自定义的文件中找(通常是当前源文件的文件夹里面) ,如果找不到在从函数库中寻找文件,如果是自己写的头文件,建议使用#include”"。

C++的内存管理机制

在C++中,内存分成5个区,他们分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。  
栈,在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。  
堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。  
自由存储区,就是那些由malloc等分配的内存块,他和堆是十分相似的,不过它是用free来结束自己的生命的。  
全局/静态存储区,全局变量和静态变量被分配到同一块内存中,在以前的C语言中,全局变量又分为初始化的和未初始化的,在C++里面没有这个区分了,他们共同占用同一块内存区。  
常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量,不允许修改。

C语言中的malloc/free和C++中的new/delete的区别和联系

相同点:它们都是对内存进行管理。

区别1:类型
malloc/free是函数,而new/delete是关键字、操作符

区别2:作用
malloc/free只是简单的进行内存的申请和释放;new/delete除了进行内存申请和释放,还会调用对象的构造函数和析构函数进行空间的初始化和清理

区别3:参数与返回值
malloc/free需要手动计算申请内存的空间大小,而且返回值是void*,需要自己转换成所需要的类型;new/delete可以自己计算类型的大小,返回为对应的类型指针。

迭代和递归区别

递归与迭代都是基于控制结构:迭代用重复结构,而递归用选择结构。
递归与迭代都涉及重复:迭代显式使用重复结构,而递归通过重复函数调用实现重复。
递归与迭代都涉及终止测试:迭代在循环条件失败时终止,递归在遇到基本情况时终止。

不可操作的操作符

作用域操作符:::
条件操作符:?:
点操作符:.
指向成员操作的指针操作符:->,.
预处理符号:#

C++关键字mutable作用

在 C++ 中,mutable是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中,甚至结构体变量或者类对象为const,其 mutable 成员也可以被修改。
mutable 在类中只能够修饰非静态数据成员。mutable 数据成员的使用看上去像是骗术,因为它能够使 const 函数修改对象的数据成员。然而,明智地使用 mutable 关键字可以提高代码质量,因为它能够让你向用户隐藏实现细节,而无须使用不确定的东西。我们知道,如果类的成员函数不会改变对象的状态,那么这个成员函数一般会声明成 const 的。但是,有些时候,我们需要在 const 的函数里面修改一些跟类状态无关的数据成员,那么这个数据成员就应该被 mutalbe 来修饰。

引用与指针有什么区别?

1.引用必须被初始化,指针不必。
2.引用初始化以后不能被改变,指针可以改变所指的对象。
3.不存在指向空值的引用,但是存在指向空值的指针。
4.引用是变量的一个别名,内部实现是只读指针

什么是黑盒测试和白盒测试?

白盒测试:
是通过程序的源代码进行测试而不使用用户界面。这种类型的测试需要从代码句法发现内部代码在算法,溢出,路径,条件等等中的缺点或者错误,进而加以修正。

黑盒测试:
是通过使用整个软件或某种软件功能来严格地测试, 而并没有通过检查程序的源代码或者很清楚地了解该软件的源代码程序具体是怎样设计的。测试人员通过输入他们的数据然后看输出的结果从而了解软件怎样工作。在测试时,把程序看作一个不能打开的黑盆子,在完全不考虑程序内部结构和内部特性的情况下,测试者在程序接口进行测试,它只检查程序是否能适当地接收和正确的输出。

你知道的类模版有哪些

vector 向量
string 字符串
list 列表
queue 队列
map 映射
set 集合
stack 栈

new可以搭配free吗,为什么?

可以,但不安全,通过 free 调用释放 new 申请的内存并不总是能正确的释放所有申请的内存。因为使用 free 方法释放内存时并不会调用实例的析构函数,此时如果实例中有动态申请的内存将因为析构函数没有被调用而没有得到释放,从而导致内存泄漏。而通常你不一定总能知道该类中是否使用了动态内存,因此最佳的做法是 new 与 delete 搭配使用。

怎么查看内存泄漏

第一:良好的编码习惯,尽量在涉及内存的程序段,检测出内存泄露。当程式稳定之后,在来检测内存泄露时,无疑增加了排除的困难和复杂度。使用了内存分配的函数,一旦使用完毕,要记得要使用其相应的函数释放掉。

第二:将分配的内存的指针以链表的形式自行管理,使用完毕之后从链表中删除,程序结束时可检查改链表。防止出现野指针。

第三:Boost 中的三种智能指针。

什么是内存溢出

内存溢出是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于虚拟机能提供的最大内存。 引起内存溢出的原因有很多种,常见的有以下几种:
  1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据;
  2.集合类中有对对象的引用,使用完后未清空,使得JVM不能回收;
  3.代码中存在死循环或循环产生过多重复的对象实体;
  4.使用的第三方软件中的BUG;
  5.启动参数内存值设定的过小;

内存溢出的解决方案

第一步,修改JVM启动参数,直接增加内存。(-Xms,-Xmx参数一定不要忘记加。)  
第二步,检查错误日志,查看“OutOfMemory”错误前是否有其它异常或错误。 
第三步,对代码进行走查和分析,找出可能发生内存溢出的位置。重点排查以下几点:
  1.检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。
  2.检查代码中是否有死循环或递归调用。   
3.检查是否有大循环重复产生新对象实体。   
4.检查对数据库查询中,是否有一次获得全部数据的查询。一般来说,如果一次取十万条记录到内存,就可能引起内存溢出。这个问题比较隐蔽,在上线前,数据库中 数据较少,不容易出问题,上线后,数据库中数据多了,一次查询就有可能引起内存溢出。因此对于数据库查询尽量采用分页的方式查询。   
5.检查List、MAP等集合对象是否有使用完后,未清除的问题。List、MAP等集合对象会始终存有对对象的引用,使得这些对象不能被GC回收。  
第四步,使用内存查看工具动态查看内存使用情况

函数指针与指针函数分别是什么

指针函数本质是一个函数,其返回值为指针。
int *fun(int x,int y);

函数指针本质是一个指针,其指向一个函数。
int (*fun)(int x,int y);

C++11新特性了解吗

1、新增容器std::array 保存在栈内存中,相比堆内存中的 std::vector,我们能够灵活的访问这里面的元素,从而获得更高的性能。
2、auto 和 decltype 这两个关键字实现了类型推导
3、替代NULL的nullptr
4、三种智能指针帮助内存管理(说一下名称):unique_ptr、shared_ptr 和 weak_ptr。
5、C++11 引入了基于范围的迭代写法,比如基于范围的for循环,用一个冒号就可实现遍历,我们拥有了能够写出像 Python一样简洁的循环语句。

接口和抽象类的区别

接口中所有的方法隐含的都是抽象的。而抽象类则可以同时包含抽象和非抽象的方法。
类可以实现很多个接口,但是只能继承一个抽象类。
类如果要实现一个接口,它必须要实现接口声明的所有方法。但是,类可以不实现抽象类声明的所有方法,当然,在这种情况下,类也必须得声明成是抽象的。
抽象类可以在不提供接口方法实现的情况下实现接口。
Java接口中的成员函数默认是public的。抽象类的成员函数可以是private,protected或者是public。

struct 和 class的区别

struct作为数据结构的实现体,它默认的数据访问控制是public的;而class作为对象的实现体,它默认的成员变量访问控制是private的。

预编译在做些什么事情?

预编译又称为预处理,是做些代码文本的替换工作。主要处理#开头的指令,比如拷贝#include包含的文件代码,#define宏定义的替换,条件编译等。就是为编译做预备工作的阶段。

可执行文件编译过程

C/C++面试题_第4张图片

动态库和静态库?

静态库特点总结如下:
静态库对函数库的链接是放在编译时期完成的。
程序在运行时与函数库再无瓜葛,移植方便。
浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。
文件后缀常为(.a、.lib)。

动态库特点总结如下:
动态库把对一些库函数的链接载入推迟到程序运行的时期。 
可以实现进程之间的资源共享。
将一些程序升级变得简单,直接改动态库即可。
文件后缀常为(.so、.dll)。

堆和栈的区别,以及为什么栈效率高

堆是由低地址向高地址扩展;栈是由高地址向低地址扩展。
堆中的内存需要手动申请和手动释放;栈中内存是由OS自动申请和自动释放,存放着参数、局部变量等内存。
堆中频繁调用malloc和free,会产生内存碎片,降低程序效率;而栈由于其先进后出的特性,不会产生内存碎片。
堆的分配效率较低,而栈的分配效率较高。

栈的效率高的原因:
栈是操作系统提供的数据结构,计算机底层对栈提供了一系列支持:分配专门的寄存器存储栈的地址,压栈和入栈有专门的指令执行;而堆是由C/C++函数库提供的,机制复杂,需要一些列分配内存、合并内存和释放内存的算法,因此效率较低。

函数参数压栈方式为什么是从右到左的?

因为C++支持可变长函数参数。正是这个原 因使得C语言函数参数入栈顺序为从右至左。具体原因为:C方式参数入栈顺序(从右至左)的好处就是可以动态变化参数个数。C 程序栈底为高地址,栈顶为低地址。
函数最左边确定的参数在栈上的位置必须是确定的,否则意味着已经确定的参数是不能定位和找到的,这样是无法保证函数正确执行的。衡量参数在栈上的位置,就是离开确切的函数调用点(call f)有多远。已经确定的参数,它在栈上的位置,不应该依 赖参数的具体数量,因为参数的数量是未知的!所以只有确定的参数最后入栈才能保证它在栈中的位置是确定的。

C++中的智能指针

C++里面的四个智能指针: auto_ptr, shared_ptr, weak_ptr, unique_ptr 其中后三个是c++11支持,并且第一个已经被11弃用。

智能指针的作用:
管理一个指针,因为存在以下这种情况:申请的空间在函数结束时忘记释放,造成内存泄漏。使用智能指针可以很大程度上的避免这个问题,因为智能指针就是一个类,当超出了类的作用域是,类会自动调用析构函数,析构函数会自动释放资源。所以智能指针的作用原理就是在函数结束时自动释放内存空间,不需要手动释放内存空间。

1、auto_ptr(c++98的方案,cpp11已经抛弃)采用所有权模式。
缺点是:存在潜在的内存崩溃问题!

auto_ptr< string> p1 (new string ("I reigned lonely as a cloud.));
auto_ptr <string> p2;p2 = p1;
//auto_ptr不会报错.此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是:存在潜在的内存崩溃问题!

2、unique_ptr(替换auto_ptr)unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露(例如“以new创建对象后因为发生异常而忘记调用delete”)特别有用。采用所有权模式。

unique_ptr<string> p3 (new string ("auto"));
unique_ptr<string> p4;
p4 = p3;//此时会报错!!
//编译器认为p4=p3非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。

//编译器认为p4=p3非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。
3、shared_ptr
shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。
shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。

4、weak_ptr
weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象. 进行该对象的内存管理的是那个强引用的 shared_ptr. weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作, 它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。
weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

基类里private成员函数可以声明为虚函数吗?

可以的,不过就和私有成员创建的本意有点相反,不建议实现。

函数A调用函数B的时候,有什么需要压栈?

函数的参数,压栈顺序是从右往左的。还有函数B内部定义的变量。

数组和指针区别?数组和链表呢?双向链表和单向链表?

数组和指针区别:
1、把数组作为参数传递的时候,会退化为指针
2、数组名可作为指针常量
3、数组是开辟一块连续的内存空间,数组本身的标示符代表整个数组,可以用sizeof取得真实的大小;指针则是只分配一个指针大小的内存,并可把它的值指向某个有效的内存空间

数组和链表区别:
不同:
链表是链式的存储结构;数组是顺序的存储结构。
链表通过指针来连接元素与元素,数组则是把所有元素按次序依次存储。
链表的插入删除元素相对数组较为简单,不需要移动元素,且较为容易实现长度扩充,但是寻找某个元素较为困难;数组寻找某个元素较为简单,但插入与删除比较复杂,由于最大长度需要再编程一开始时指定,故当达到最大长度时,扩充长度不如链表方便。
相同:
两种结构均可实现数据的顺序存储,构造出来的模型呈线性结构。

双向链表和单向链表区别:
单向链表包含两个域,一个是信息域,一个是指针域。也就是单向链表的节点被分成两部分,一部分是保存或显示关于节点的信息,第二部分存储下一个节点的地址,而最后一个节点则指向一个空值。

双向链表每个节点有2个链接,一个是指向前一个节点(当此链接为第一个链接时,指向的是空值或空列表),另一个则指向后一个节点(当此链接为最后一个链接时,指向的是空值或空列表)。意思就是说双向链表有2个指针,一个是指向前一个节点的指针,另一个则指向后一个节点的指针。

vector底层实现?

底层数据结构是一个动态数组。默认构造的大小是0, 之后插入按照1 2 4 8 16 二倍扩容。注(GCC是二倍扩容,VS13是1.5倍扩容。原因可以考虑内存碎片和伙伴系统,内存的浪费)。扩容后是一片新的内存,需要把旧内存空间中的所有元素都拷贝进新内存空间中去,之后再在新内存空间中的原数据的后面继续进行插入构造新元素,并且同时释放旧内存空间,并且,由于vector 空间的重新配置,导致旧vector的所有迭代器都失效了。

vector的初始的扩容方式代价太大,初始扩容效率低, 需要频繁增长,不仅操作效率比较低,而且频繁的向操作系统申请内存容易造成过多的内存碎片,所以这个时候需要合理使用resize()和reserve()方法提高效率减少内存碎片的。

resize():

void resize (size_type n);
void resize (size_type n, value_type val);

1、resize方法被用来改变vector中元素的数量,我们可以说,resize方法改变了容器的大小,且创建了容器中的对象;
2、如果resize中所指定的n小于vector中当前的元素数量,则会删除vector中多于n的元素,使vector得大小变为n;
3、如果所指定的n大于vector中当前的元素数量,则会在vector当前的尾部插入适量的元素,使得vector的大小变为n,在这里,如果为resize方法指定了第二个参数,则会把后插入的元素值初始化为该指定值,如果没有为resize指定第二个参数,则用默认值填充新位置,一般为0;
4、如果resize所指定的n不仅大于vector中当前的元素数量,还大于vector当前的capacity容量值时,则会自动为vector重新分配存储空间;

reserve():避免了频繁的申请内存空间,造成过多内存碎片。

void reserve (size_type n);

1、reserve的作用是更改vector的容量,使vector至少可以容纳n个元素。
2、如果n大于vector当前的容量,reserve会对vector进行扩容。其他情况下都不会重新分配vector的存储空间。
3、reserve方法对于vector元素大小没有任何影响,不创建对象。

vector中数据的随机存取效率很高,O(1)的时间的复杂度,但是在vector 中随机插入元素,需要移动的元素数量较多,效率比较低。

vector与list的区别?

vector拥有一段连续的内存空间,因此支持随机存取,如果需要高效的随即存取,而不在乎插入和删除的效率,使用vector。
vector和数组类似,它拥有一段连续的内存空间,并且起始地址不变,因此它能非常好的支持随机存取(使用[]操作符访问其中元素),但由于它的内存空间是连续的,所以在中间进行插入和删除会造成内存块的拷贝(复杂度是O(n)),另外,当该数组后的内存空间不够时,需要重新申请一块足够大的内存并进行内存的拷贝。这些都影响了vector的效率。

list拥有一段不连续的内存空间,因此不支持随机存取,如果需要大量的插入和删除,而不关心随即存取,则应使用list。list是由数据结构中的双向链表实现的,因此它的内存空间可以是不连续的。因此只能通过指针来进行数据的访问,这个特点使得它的随机存取变的非常没有效率,需要遍历中间的元素,搜索复杂度O(n),因此它没有提供[]操作符的重载。但由于链表的特点,它可以以很好的效率支持任意地方的删除和插入。

你可能感兴趣的:(C/C++)