博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
C++基础知识面试考点归纳
阅读量:4048 次
发布时间:2019-05-25

本文共 19614 字,大约阅读时间需要 65 分钟。

2020年秋招期间自己归纳总结的牛客上有关C++常见的面试考点。通过秋招面试情况来看,大部分问题确实有被问到,对自己也有非常大的帮助。不建议直接看我写的回答,而是带着问题去总结自己的回答。相信坚持就会有好结果。

58个问题,共计一万五千多字。

1、 c++重载(overload)和重写(override)的区别

重载是一组函数名相同但是参数特征标不同的方法,特征:
方法名必须相同,参数列表必须不同,返回参数可以不同。其中返回参数可以不同,体现在函数重载是在编译阶段完成,是一个静态编译过程,而c++调用函数是可以忽略返回值的,所以不能作为判断依据。(构造函数,模板函数)(静态)
重写是派生类重写基类的虚函数,特征:
只有虚方法和抽象方法才能够被重写,必须具备有相同的函数名,参数列表和相同的返回值类型。返回参数在一种情况下可以不同,即返回类型协变,体现在可以用基类的指针去指向派生类对象,所以返回的可以是基类指针也可是派生类指针,是一个动态的绑定过程。所以重写是在运行阶段检查的。(动态)

2、 c++多态的体现方式

同一种操作对于不同的对象,可以有不同的解释,产生不同的执行结果
主要有两种类别:
一种是编译时的多态(静态编译,静态绑定),通过重载实现。系统根据参数列表不同在一组同一函数名的方法中选择一个执行;
另一种是运行时的多态(动态编译,动态绑定),通过重写实现。主要是因为我们可以用基类指针指向派生类对象,就需要代码在运行时确定指向的对象具体是哪一种对象,再执行相应的操作。

3、 关键字const

Const作用:
定义常量,编译期间可以进行静态数据类型的安全检查;
修饰函数的传入参数:经常与引用一起使用,常用在修饰用户自定义的数据类型时,能够避免在传参数调用构造,复制和析构函数,加快运行效率,同时也能避免原始数据被修改。Const形参可以传入非const和const实参;
修饰函数的返回值时:返回值不能被直接修改,而且只能复制给const对象;
修饰类成员函数时:任何不会修改数据成员的成员函数都应该设置为const,形式比较特殊,放在函数的末尾。

4、 const和#define的区别

宏定义没有数据类型,只做文本替换。Const有数据类型,且会进行安全检查;

5、 static关键字

static修饰的变量为静态变量,只被初始化一次,在函数体内的static变量值依据上一次的结果,访问方式为类名.变量名;
在函数体外部定义的静态变量,可以被该文件下的所有函数调用,无外部链接性,是一个全局变量;
Static修饰的函数,可以被模块内的其他函数调用;
此外,定义在类中的静态变量和函数不是类的成员,没有this指针,不占用类的内存,并且要在类的外部进行初始化;
Static只能修饰内部类

6、 static与普通的全局变量有什么区别

static定义的全局变量只能被初始化一次,无外部连接性,只能被该文本下的所有函数使用。而普通全局变量,一般声明在头文件中,可以被所有包含该头文件的文件函数初始化并调用。
Static定义的函数,与普通函数的区别除了同变量相同之外。Static定义的函数内存中只有一份,而普通函数,调用一次就会有一个副本。

7、 内存泄漏解决办法

存在两个方面:手动开辟的内存空间没有进行释放,其次含有派生类的基类析构函数不是虚函数
解决办法:记得每次手动释放,使用智能指针,含有派生类的基类析构函数设置为虚函数。还可以选用智能指针

8、 智能指针

智能指针是作为内存管理工具。为了防止在动态开辟和释放内存的过程中,由于人为疏漏忘记释放内存而导致的内存泄漏问题。智能指针本质上是一个类,当作用域超过范围时,会自动的调用析构函数,释放内存空间,防止内存泄露。
auto_ptr:一种独占式拥有,当将一个对象的指针赋值给另一个变量时,会导致所有权的转让(即 一个对象只能有一个智能指针指向)但是不会报错。因此之前的指针变量会指向不定向,当访问其内存时就会出现问题。
unique_ptr:也是一个非常严格的独占式拥有,即一个对象只能自己拥有,但解决了auto_ptr的问题,在编译器会直接报错。其次可以将unique_ptr临时的右值赋值给unique_ptr对象。或者用move()函数实现所有权转让。
shared_ptr:实现共享式拥有,即一个对象可以被多个该指针指向。只有当指向该对象的所有指针都销毁时,该对象才会被释放。内部函数有use_count()返回指向的个数;unique()判断是否独占;swap()交换两个指向对象;对shared_ptr进行初始化时不能将一个普通指针直接赋值给智能指针,因为一个是指针,一个是类。可以通过make_shared函数或者通过构造函数传入普通指针。并可以通过get函数获得普通指针。
weak_ptr是一种不控制生命周期的智能指针。指向一个shared_ptr管理的对象,目的是配合shared_ptr进行对象管理。他的创建和消灭不会引起use_count()的增加。是为了解决shared_ptr相互引用造成的死锁问题。

9、 C++四种类型转换

const_cast:用于将const转换成非const
static_cast:用于各种隐式转换,非const转const。能用于多态向上转换,向下转换也行但是不安全;
dynamic_cast:动态类型转换,只能用于含有虚函数的类,用于类层次间的向上或者向下转换。只能转指针或者引用。向下转换如果返回是指针返回null,如果是引用则抛出异常。
reinterpret_cast:万能转换符,因为万能所以容易出错,尽量少用。

10、 指针和引用有什么区别

指针有自己的一块空间而引用只是变量的一个别名,因此用sizeof求空间大小的时候指针为4,而引用是引用对象的大小,因此使用++运算符含义也不同;
指针初始化时候可以设置为null,而引用必须初始化一个确定对象,因此指针可以指向不同的对象,引用不能中途易主;
可以有const指针,但是没有const引用;
如果函数返回动态分配的内存空间时,必须使用指针,引用可能引起内存泄漏。

第二次补充:是什么(本质),怎么用及问题(初始化(引用好于指针(野指针)),函数传入参数(两个都好),返回值(指针好于引用(内存泄漏))),适用场景

11、 什么是野指针,怎样预防野指针

两种情况:申明未初始化的指针变量,指向已删除对象的指针变量
预防:初始化时赋初值,指向删除对象的指针设为null

12、 什么是函数指针

函数指针就是指向函数的指针,编译时每个函数都有一个入口地址,函数指针就是指向该入口地址的指针。
可用作调用函数或者做函数的参数,比如回调函数

13、 虚函数表具体是怎样实现多态的

多态,对同一输入和表达式得到多种形态的输出。具体来说,就是通过父类指针和虚函数和重写机制去调用子类的成员函数。
因此实现多态必须有:继承和虚函数机制(或者说多态是通过虚函数实现的)
什么是虚函数表:对于含有虚函数的类都会有一个虚指针(属于类的空间)指向虚函数表(属于类外空间),用于在运行期间动态的绑定成员函数。
虚函数表在不同继承情况下是否不同:
对于单继承:子类虚表指针所指向的虚函数表的内容首先是父类的虚函数地址,再排布子类的虚函数地址。如果子类虚函数有对父类虚函数的重写,则用子类该虚函数覆盖掉父类的虚函数地址。
对于多继承(非菱形继承):有多少个父类就会在子类空间中形成几个虚表指针,排布顺序按照声明顺序。同时会将子类虚函数指针排布在第一个续表后面,如果子类虚函数对父类虚函数重写,则用子类该虚函数覆盖掉所有父类的虚函数地址。
对于虚继承(解决多重继承中的冲突问题):父类的成员变量以及虚函数都放在子类内存空间的最后。父类的虚表只会出现一次
https://www.cnblogs.com/jerry19880126/p/3616999.html
https://blog.csdn.net/li1914309758/article/details/79916414

14、 malloc和new的区别

class A {…};
A* Ptr = new A;
A* Ptr = (A*)malloc(sizeof(A));
new和delete是C++关键字,需要编译器支持,而malloc为标准库函数;
malloc开辟空间时需要显示指定内存大小,而new根据对象大小自动分配;
new分配成功的时候是严格返回的对象类型的指针,而malloc是返回的(void*)需要进行强制的类型转换。
new分配失败的时候会返回异常bac_alloc,而malloc分配失败时会返回null。因此在使用malloc时候,会经常在其后使用是否为NULL判断语句,用来判断内存是否正常开辟;
是否调用构造函数和析构函数:使用new操作符来分配对象内存会有三个步骤:首先调用operator new函数,分配一个足够大的未命名空间来存储特定类型的对象。然后,编译器运行构造函数,并为其传入初始值。最后构造完成,返回一个指向该对象的指针。使用delete释放对象会有两个步骤:首先调用对象的析构函数,其次编译器调用operator delete释放内存空间。因此使用new和delete能够调用构造和析构函数并且初始化对象,而malloc和free不能。
对数组的处理:可以使用new和delete[]创建和释放数组对象。而malloc无法判断开辟的空间是存放数组还是单个对象,需要我们自定义数组的整个内存大小。
opeartor new /operator delete可以被重载。而malloc/free并不允许重载。
能否重新扩充内存,当发现当前内存不够时,可以使用remalloc进行内存扩充,两种情况,一是当前指针所指向内存有足够大的内存空间时,原地扩大可分配的内存地址,并返回原内存地址指针。当连续内容不够时,按新内存大小开辟一个新内存,并将原始数据复制到新内存中,然后释放原来的存储区域,并且返回新内存的地址指针。new没有这种机制。

15、 友元函数是什么,及其作用

类的友元函数是一种声明在类的内部(可以在类的内部定义,也可以在类的外部定义。在类外定义时,不需要加关键字“friend”也不需要加作用域符“::”),但是不属于类的成员函数的一种函数。但他被赋予了访问类的所有私有成员和保护成员的权限。能使其他类的成员函数直接访问该类的私有变量。即:允许外面的类或函数去访问类的私有变量和保护变量,从而使两个类共享同一函数。
由于友元函数不属于类成员函数,因此没有this指针。因此在访问非static成员时,要使用对象作为参数。
与成员函数不同,友元函数是不能够被继承的;
友元函数虽然能够提高访问的效率,但它破坏了封装机制。

16、 对于友元破坏了封装机制这种说法你怎么看?

表面上看,C++确实通过友元函数直接访问了类私有成员和保护成员。也确实破坏了类的封装性。但从逻辑上来说,我们最终的目的是为了想在内的外部去访问类的私有或保护成员。如果没有引入友元机制,有两种解决办法,一种是在类中定义私有成员的公共接口。另一种是将想访问的私有成员或者保护成员变成公有。前者无疑增加了调用的复杂性,而且同样也将私有成员暴露在外面。后者使这种暴露更加彻底。而友元的定义受类的控制,正确的使用,无疑能够更加有效的提升类的封装性。

17、 delete[]和delete的区别

通俗答案来说,delete是释放掉new分配的单个对象指针指向的内存,而delete[]是释放掉对象数组指针指向的内存。如果用delete去释放数组指针对象,则会造成内存的泄露。
其实,这个问题应该被分为两个方面回答:
当new[]定义的是简单类型数组对象,例如(int,double,char)等。使用delete和delete[]效果是一样的。都不会造成资源的泄露。原因在于,分配简单类型的时候,系统会记忆和管理分配的内存大小。并保存分配内存大小等信息。而且简单类型不会调用析构函数。因此两种方式不存在差别。
当new[]定义的是自定义类类型的对象时。调用delete,同样也会释放掉整个数组大小的内存块,但是只能够调用数组第一个对象的析构函数,而后续对象析构函数不被调用。
问题来了,既然不加方括号也能完整释放内存,那不就没多调用几个析构函数吗?万一析构函数需要释放系统资源呢?比如文件?线程?端口?这些东西使用了而不释放将会造成严重的后果。因此,虽然内存完整的释放了,但是有时候不调用析构函数则会造成潜在的危险。

18、 类和结构体有什么区别

在c++中,为结构体也添加了类的属性,因此结构体能有成员函数,也能继承也能实现多态。那他们区别主要体现在哪里:
默认的访问权限:class内部成员变量和函数默认为私有的,继承时默认为也是私有的。而struct默认为两个都是公有的。struct可以继承class,同样class也可以继承struct。默认是public继承还是private继承,取决于子类而不是父类。
“class”这个关键字还用于定义模板参数,就像“typename”。但关键字“struct”不用于定义模板参数。

19、 有哪些继承方式及访问权限

公有继承(public),保护继承(protected),私有继承(private)
共有继承不改变基类的访问权限,私有继承基类成员全部变为子类的私有成员,保护继承的基类共有成员变成子类的保护成员,其余不变。
子类内部访问基类的访问权限:在子内的内部都能够直接访问基类的保护成员和共有成员,都不能直接访问基类的私有成员。
子类的子类访问基类的权限:子类的子类可以直接访问基类共有继承和保护继承下的共有成员和保护成员。而私有继承形式下的基类所有成员不可访问。
子类的对象访问基类的权限:只能直接访问公有继承下的基类共有成员,其余继承方式和权限都不可访问。

20、 单重继承和多重继承

单重继承:一个派生类只有一个基类;多重继承:一个派生类含有多个基类。
在上一个问题中我们讨论了单重继承的三种方式,不再赘述,本题只讨论多重继承的问题和解决办法。
多重继承又可以分为两种类别。一种是派生类直接继承多个基类。这里面存在的题是不同基类之间有可能存在有相同的成员变量或者函数,因此派生类中对这些变量和函数就会产生访问歧义。解决办法就是加上特定的域解析符(基类名::数据成员名(或者函数))明确指定要访问的类别。还有一种也叫作“菱形继承”,即两个子类分别继承自同一个基类,子类的子类又继承自两个子类。就会导致基类被两次构造的问题。这个时候引入虚继承机制,基类只会在最开始被调用构造函数一次,其子类共享同一个基类对象,并且忽略虚基类的其他子类对虚继承的构造函数的调用,从而保证了虚基类的数据成员不会被多次初始化。

21、 STL中的迭代器?什么情况下会迭代器失效?

迭代器是一个可对其执行类似于指针操作的对象,可以认为是广义指针。引起迭代器失效的原因依据不同的存储容器,主要可以分为两大类:
一是对容器的插入删除操作所导致的元素移位:对于顺序存储容器(例如vector, string)为了实现快速的查找,往往将数据存放在一个连续的内存块。因此,当插入一个元素或者删除一个时,会导致数据的移位。因此vector和string插入和删除位置之前的迭代器有效,之后的迭代器失效。deque(双端队列)实现类似于vector,支持随机访问,但在首位插入元素的时间复杂度是O(1)。因此当首位元素插入或删除时,不会引起迭代器失效。当在其他位置插入删除会导致所有的失效。对于list和forward_list,其是非线性存储过程,任意地方的插入删除操作不会导致迭代器的失效。
二是由于容器内存不够所导致的重新分配内存和复制元素所导致的:针对vector等动态分配容器。当预分配的存储空间不够时,会按照一定的分配规则扩大内存的分配。当其后有足够的空间满足分配要求时,不会移动复制。当不满足要求时,就会在其他内存区开辟空间,并将已有的数据复制过去,因此导致迭代器整体失效。

22、 C++11的新特性?

统一的初始化方式:可以将所有的内置类型和类内类型使用大括号进行初始化,并且等号可有可无。
简化的类型声明:auto和decltype,前者由编译器自动判断变量的类型。后者将变量的类型声明为表达式指定的类型。常用在定义模板时十分有用。如:
template<typename T, typename U>
void funt(T t, U u){
decltype(t*u) tu;
}还有一种是返回类型后置。
using替换掉了typedef;
nullptr替换掉了null;
可以在类定义成员变量时进行初始化,利用等号或者大括号。
引入智能指针unique_ptr, shared_ptr, weak_ptr解决内存分配释放及泄露问题;
enum class / enum struct 替换了enum,要求必须进行显示限定,以免发生名字冲突,更加安全。
基于范围的for循环,可实现单纯读取,或者利用引用修改元素的值。
新的STL容器:forward_list, unordered_map, unordered_multimap, unordered_set, unordered_multiset。还新增模板array,指定元素类型和固定的元素个数。
新的STL迭代器方法:cbegin(), cend()是begin()和end()的const版本,及不能进行值的修改。此外crbegin()和crend()是rbegin()和rend()的const版本。
引入了右值引用“&&”,可以直接引用常量,或者得到常量值的地址;
移动语义与右值引用。上面提到了右值引用,貌似没有什么实质的作用和改进。当考虑函数返回值为一个非常大的内存数据块时,这个时候可能会开辟临时内存将数值拷贝完毕后再删除。移动语义就是考虑不移动原始内存数据,直接将所有权转交给新的对象。
默认的方法(default)和禁用(delete)的方法:当自定义了构造函数,复制函数等时,编译器就不会提供默认的构造函数了。此时就可以如下定义(例:Someclass() = default)。也可以用delete禁止编译器使用特定的方法(例:void redo(int) = delete)。
委托构造函数:在一个构造函数的定义中,使用另一个构造函数,委托使用成员初始化语法的变种。
继承构造函数:派生类能够继承基类构造函数的机制。
标识符override和final,防止继承机制中的虚函数因特征标不匹配所导致的函数被隐藏的问题。通过显示指定要覆盖的虚函数,使得程序更加安全直观。final禁止派生类覆盖特定的虚方法。
Lambda表达式是用来定义并创建匿名的函数对象。主要由以下几部分构成:中括号中的捕获参数(有传值和传引用的方式,当传值的时候,需要加上关键字“mutable”才能修改值,并在定义时传入值),小括号中的函数参数,以及箭头指向的返回参数,和大括号包括的函数体。主要目的是为了随用随定义简短的函数表达式,或是用作STL中提供谓词函数的参数。(count_if(), sort(), generate())

23、 strcpy和memcpy的区别

复制的内容不同,strcpy只能复制字符串,而且会复制字符串的结束符,而memcpy可以复制任意类型的内容。
复制的方法不同:strcpy不需要指定复制的长度,当遇到字符串结束符“\0”才会停止复制,因此容易溢出。而memcpy需要指定字符串的长度值,第三个参数为复制的字符串长度。

24、 struct和union的区别

结构体和共用体都是由多个不同数据类型组合的,在任意时刻,共用体只能存放一个被选中的变量,而结构体可以存放所有数据类型的变量;
对于共用体的值进行重写,原来的值就会被覆盖掉。而对于结构体的不同成员变量之间赋值是不会相互影响的。
分配的内存大小不同。共用体的内存大小为其内部数据类型变量所占空间最大的值,或其倍数进行分配。而结构体是所有数据类型的偏移累加,并按照一定的对齐规则进行对齐。

25、 编译原理: C++代码到可执行文件的过程

1) 预处理:处理宏定义(#define),条件编译指令(#ifdef, #else),头文件;
2) 编译:检查语句是否符合语法规则,生成汇编代码;
3) 优化程序:软件层面优化和考虑硬件寄存器方面的优化;
4) 汇编程序:把汇编语言翻译成机器语言;
5) 链接程序:静态链接和动态链接。
https://blog.csdn.net/weixin_40756041/article/details/88052207
https://blog.csdn.net/lanmolei814/article/details/38908753?utm_source=blogxgwz9

26、 构造函数和析构函数中可以调用虚函数吗

实现编译和运行上是没有问题的,但“Effective c++”不建议这样做,达不到我们预期的效果。“构造和析构期间不要调用虚函数,因为这类调用不会下降到其子类”
什么时候会出现:希望用基类多态实现对子类同一操作过程的特异性结果。
这样做的问题:当构造一个子类对象时,先会调用基类构造函数,此时子类对象还不存在。那基类构造函数中的虚函数,失去了虚函数的性质,调用的还是基类该函数本身。当析构一个子类对象时,先调用子类的析构函数,此时子类中的虚方法都不存在了,再去调用基类构造函数时的虚函数,也就失去了虚函数的意义。所以还是调用的基类虚函数本身。
解决办法:将子类必要的构造信息,向上传递至基类。由基类的构造函数调用含有该信息的非虚函数。(explicit, static)

27、 内存分区 、堆区和栈区的区别、特点

C++内存有五个分区:堆,栈,静态存储区,常量存储区和程序代码区
栈(stack):指那些由编译器在需要的时候分配,不需要时⾃动清除的变量所在的存储区,效率高,分配的内存空间有限,形参和局部变量分配在栈区,栈是向地地址生长的数据结构,是一块连续的内存。
堆(heap):由程序员控制内存的分配和释放的存储区,是向高地址生长的数据结构,是不连续的存储空间,堆的分配(malloc)和释放(free)有程序员控制,容易造成二次删除和内存泄漏
静态存储区(static):存放全局变量和静态变量的存储区,初始化的变量放在初始化区,未初始化的变量放在未初始化区。在程序结束后释放这块空间。
常量存储区(const):存放常量字符串的存储区,只能读不能写,const修饰的局部变量存储在常量区(取决于编译器),const修饰的局部变量在栈区。
程序代码区:存放源程序二进制代码。

28、 为什么要进行字节(内存)对齐?有哪些字节(内存)对齐准则

为什么:CPU读取内存是从对齐地址开始;一次读取的内存大小是4字节或者8字节(double(8),起始放在5,读两次)
1) 结构体变量的首地址能够被其对齐字节数大小所整除。
2)结构体每个成员相对结构体首地址的偏移都是成员大小的整数倍,如不满足,对前一个成员填充字节以满足。
3)结构体的总大小为结构体对齐字节数大小的整数倍,如不满足,最后填充字节以满足。

29、 C++重载函数为什么不能用返回值来区别呢

保持解析操作符或函数调用时,独立于上下文(不依赖于上下文)
我们也可能调用一个方法,同时忽略返回值;我们通常把这称为“为它的副作用去调用一个方法”
函数的返回值只是作为函数运行之后的一个“状态”,他是保持方法的调用者与被调用者进行通信的关键。并不能作为某个方法的“标识”

30、 容器vector使用的时候应该注意什么问题

  1. 时间上:提前分配足够空间以免不必要的重新分配和复制代价(reserve()函数);
  2. 空间上:使用shrink_to_fit释放多余内存(erase和clear不能释放内存);
  3. 时间上:复制vector时赋值(=)快于insert和复制构造函数快于push_back();
  4. 遍历vector时选用下标和at(),迭代器只是为了保证通用性,效率相对较低;
  5. 避免在vector前部插入元素,如果需要经常插入,换容器;
  6. 在尾部插入元素时用emplace_back好于push_back;
    https://www.cnblogs.com/Braveliu/p/6622298.html
    https://www.cnblogs.com/Braveliu/p/6264543.html

31、 接口继承和实现继承有什么区别

接口继承:派生类只继承基类的接口(声明);
实现继承:派生类同时继承基类的接口和实现;
声明纯虚函数只是为了让派生类只继承函数接口,也就是上面说的接口继承。通俗说,纯虚函数就是要求其继承类必须含有该函数接口,并对其进行实现。是对继承类的一种接口实现要求,但并不提供缺省操作,各个继承类必须分别实现自己的操作。(like-a,例如人都可以“说话”,不同的语言)
声明虚函数(impure virtual)的目的是让继承类继承该函数的接口和缺省实现。与纯虚函数唯一的不同就是其为继承类提供了缺省操作,继承类可以不实现自己的操作而采用基类提供的默认操作。(has-a,基本的大家都能说话,rap,口技)
声明非虚函数(non-virtual)的目的是为了令继承类继承函数接口及一份强制性实现。相对于虚函数来说,非虚函数对继承类要求的更为严格,继承类不仅要继承函数接口,而且也要继承函数实现。也就是为继承类定义了一种行为。(is-a)

32、 STL容器了解哪些

分为两大类序列容器和关联容器
vector是一个序列式容器,里面的底层实现是一个顺序表结构,可以动态增长长度的数组;(随机访问,边界检查)注意事项见上;
deque的底层实现是一个链表数组,deque内部采用分段连续的内存空间来存储元素,序列式容器;(随机访问,头尾插时间固定)
list是一个双向链表,序列式容器。(插入任意位置时间固定,不能随机访问)
forward_list前向链表,只需要正向迭代器,结构更简单和紧凑,功能也更少;
queue是一个适配器类,不允许随机访问和遍历队列,只能在尾部插入和头部删除元素;
priority_queue是一个适配器类,底层类实现是vector,将最大元素排列到队首,可以定义比较方式作为可选项;
stack适配器类,底层是vector,只能在头部插入和删除,不支持随机访问和遍历;
array<T,N> 模板定义了一种相当于标准数组的容器类型。它是一个有 N 个 T 类型元素的固定序列。除了需要指定元素的类型和个数之外,它和常规数组没有太大的差别。显然,不能增加或删除元素。
Valarry,提供了一种用于数学运算的数组类型,可以用max, min, sum等,也可以用slice提取子数组等。

接着可以谈谈迭代器操作,实现对上述容器的统一管理方式。(效率问题,什么时候用比较好)不同容器之间的相互拷贝,用于统一的函数(sort(), count_if(),for_each()等)

map 字典 映射 ,map是一个关系式容器 ,以模板(泛型)方式实现 ,底层通常是由一颗红黑树组成,第一个可以称为键(key),第二个可以称为该键的值(value),在map内部所有的key都是有序的,并且不会有重复的值,后面的key会覆盖掉前面的key。

multimap, 多表映射的结构与map 相似,但是多表映射允许有多个重复的键值
unordered_map,无序映射的key的值不会进行排序,但是会去除重复的key值。内部结构是哈希表,查找为O(1),效率高。哈希表的建立耗费时间。对于频繁查找的问题,用unordered_map更高效。
unordered_multimap,该容器底层是哈希表实现,里面的key既不会去重,也不会排序,用法与unordered_map类似,虽然不会排序,但重复的数据也会排列在一起。
Set,关联集合,可翻转可排序且键是唯一的。说到集合,关键的操作就是求交集,并集和差集,set_union(), set_intersection(), set_difference()。还有lower_bound()和upper_bound()将键值作为参数,返回一个迭代器。
unorder_set(), multiset(), unorder_multiset()
https://blog.csdn.net/qq_45893999/article/details/106595166

33、 C++ 面向对象说一下

以问题为导向进行分析:面向过程时代,以机器处理的运行的视角去编写程序。要达到一个目的第一步做什么,第二步做什么…这样导致代码中会出现很多的子函数与数据项。当我们的工程开始庞大,需要去增删改查某些功能时,就十分困难。因此第一个想法,就是将函数分“类”管理。类是数据和方法的集合,因此现在的问题重点就在于如何设计类和组织类间关系。不同的数据可以造就不同的实例(对象)。如果单纯将面向过程中的函数和数据以一种具有相似特性关系为原则组合在一起,那是认识的第一步,这时的类是静态类,方法是静态方法。到这一步问题出现的问题就是,很多类具有公共的数据和方法。因此解决办法就是将共有的部分和方法抽象提取出来,作为基类,再由父类去继承基类。那这就引出了关键词“继承”。继承的目的有两个,一是重用基类的方法,减少冗余,其次是实现多态,即用一个接口去实现同一函数的不同功能。这一部分就涉及到了(虚函数,纯虚函数,继承的几种方式,单继承,多继承等知识)。引入这些目的就是为了让继承关系更加清晰,实现数据和方法等细节的隐藏,也就是关键词“封装”,到这为止是认识的第二步。此时就可以对面向对象有个小结:目的就是从“代码写给机器看”到“代码写给人看”的一个转变。通过类来抽象和屏蔽细节,使程序员更多精力放在整体框架设计和功能实现上。认识的第三步(设计模式“用组合实现重用,用继承实现多态”最高境界)

34、 如果不用指针和引用,子类赋值给父类,会不会引发动态绑定,为什么?

动态绑定就是多态的实现,那就是虚函数和虚函数表知识。首先说说为什么会发生动态绑定。父类指针指向的还是子类的实例,具有一个虚表指针指向虚表。虚表中已经存放了指向对应虚函数的指针。因此可以用子类指针形式实现动态绑定。如果是赋值,只是将父类中的子类变量赋值给父类变量,不会引发动态绑定过程。

35、 vector底层工作原理

其底层所采用的数据结构非常简单,就只是一段连续的线性内存空间。有三个迭代器(指针)分别指向,容器对象的起始字节位置;当前最后一个元素的末尾字节和整个 vector 容器所占用内存空间的末尾字节。配合使用这三个迭代器,就可以实现诸如首尾标识、大小、容器、空容器判断等几乎所有的功能。
此外,vector的扩容原理(见上问题),依据编译器不同,扩容的方法也不同。

36、 介绍一下RAII 机制

RAII是Resource Acquisition Is Initialization(wiki上面翻译成 “资源获取就是初始化–使用类来封装资源,在构造函数中完成资源的分配和初始化,在析构函数中完成资源的清理,可以保证正确的初始化和资源释放”)的简称,是C++语言的一种管理资源、避免泄漏的惯用法。利用的就是C++构造的对象最终会被销毁的原则。RAII的做法是使用一个对象,在其构造时获取对应的资源,在对象生命期内控制对资源的访问,使之始终保持有效,最后在对象析构的时候,释放构造时获取的资源。
由于系统的资源不具有自动释放的功能,而C++中的类具有自动调用析构函数的功能。如果把资源用类进行封装起来,对资源操作都封装在类的内部,在析构函数中进行释放资源。当定义的局部变量的生命结束时,它的析构函数就会自动的被调用;
RAII机制便是通过利用对象的自动销毁,使得资源也具有了生命周期,有了自动销毁(自动回收)的功能。(智能指针)

37、 std::function可以封装哪些实体,可以封装函数对象吗?

类模版std::function是一种通用、多态的函数封装。std::function的实例可以对任何可以调用的目标实体进行存储、复制、和调用操作,这些目标实体包括普通函数、Lambda表达式、函数指针、以及其它函数对象等。std::function对象是对C++中现有的可调用实体的一种类型安全的包裹(我们知道像函数指针这类可调用实体,是类型不安全的)。
通过std::function对C++中各种可调用实体(普通函数、Lambda表达式、函数指针、以及其它函数对象等)的封装,形成一个新的可调用的std::function对象;让我们不再纠结那么多的可调用实体。一切变的简单粗暴。
关于可调用实体转换为std::function对象需要遵守以下两条原则:
转换后的std::function对象的参数能转换为可调用实体的参数;
可调用实体的返回值能转换为std::function对象的返回值。
std::function对象最大的用处就是在实现函数回调,使用者需要注意,它不能被用来检查相等或者不相等,但是可以与NULL或者nullptr进行比较。
std::function实现了一套类型消除机制,可以统一处理不同的函数对象类型。以前我们使用函数指针来完成这些;现在我们可以使用更安全的std::function来完成这些任务。
为什么不用函数指针?从功能上来说,二者确实没什么区别,用functor的地方,基本换个写法都能用函数指针替代,那为什么还要有functor?,原因如下。(1)functor是用面向对象泛型思想实现的,其实不止functor,STL六大组件全部都是这种思想下的产物,而function pointer和STL整体架构思想并不一致。(2)functor本质上是对象,有对象就有数据封装,而函数指针只能用全局或局部变量。(3)functor的operator()重载,一般是比较轻量级的代码,可以被编译器自动内联化,当然,如果你喜欢写复杂代码,也没问题,但不推荐这么用。(4)函数对象可以有多态特性,更好的控制run-time behavior。

38、 lambda表达式介绍一下,可以引用外部变量吗?

Lambda表达式类似于内联函数,是一种结构小,紧凑的函数类型,没有函数名。使用lambda表达式的主要目的是为了对于一些被函数调用场景更加方便可读,例如c++很多通用的表达式(sort(), for_each()等)。主要由四部分组成:捕获参数,函数参数,返回值和函数体。

39、 为什么需要this指针

在 C++ 中,每一个对象都能通过 this 指针来访问自己的地址。this 指针是所有成员函数的隐含参数。因此,在成员函数内部,它可以用来指向调用对象。

40、 左值引用和右值引用的区别?

左值引用要求右边的值必须能够取地址,如果无法取地址,可以用常引用;
但使用常引用后,我们只能通过引用来读取数据,无法去修改数据,因为其被const修饰成常量引用了。
可以取地址的,有名字的,非临时的就是左值;
不能取地址的,没有名字的,临时的就是右值;
右值引用的存在并不是为了取代左值引用,而是充分利用右值(特别是临时对象)的构造来减少对象构造和析构操作以达到提高效率的目的。
所谓的移动构造函数

41、 左值引用可以直接引用右值吗?

直接不行的,可以加const限定;

42、 static_cast和强制转换的对比?

前面的是C的风格,后面的是C++的风格(也是推荐使用的)。差别在于,static_cast更安全一些,对于指针操作的话,多了一些检查,例如无关指针就无法转换,父类指针向孩子指针,无法转换,常指针向非常指针无法转换,而前面的那个则是通吃,通常是下面四个转换的合体
reinterpret_cast

43、 析构函数能设置成虚函数吗

可以,且具有派生类的基类析构函数必须为虚函数;

44、 说一下浅拷贝和深拷贝

两者的对比和问题主要是在类的复制构造函数上体现的。我们在给类初始化,函数以值的方式传入对象和返回对象,以及派生类的初始化列表初始化基类都会自动调用默认的复制构造函数。这个调用过程可以简单的理解为将新定义的类中的成员变量指向或被复制用旧的类对象。这就是所谓的浅拷贝。当类中都是一些基本成员(int double…)不会出现问题。但当类中包含有通过指针指向的自开辟的内存空间对象时(比如说数组),此时执行浅拷贝就会只将指针赋值给新对象的成员函数。也就是两个指针指向同一个内存。当旧的指针结束生命周期后,其自动调用析构函数,释放内存空间。那此时另一个成员变量指向的该地址就没有数据,就会出现错误,指向不明确。

45、 关于编译过程和静态链接动态链接说一下

链接可以在编译时由静态编译器完成,也可以在加载和运行时由动态编译器完成。编译器所处理的文件类型有三种形式:可重定位,可执行和可共享的。
链接器的主要两个任务是符号解析和重定位。符号解析就是将目标文件中的所有全局变量都绑定到一个唯一的定义。重定位就是确定每个符号的最终地址,并修改对这些目标的引用。
静态连接器在解析多重定义的全局符号时遵循三个原则:(函数和已初始化的全局变量是强符号,未初始化的是弱符号)
1) 不能同时存在多个同名的强符号;
2) 当有一个强符号和多个弱符号时,选择强符号;
3) 当有多个弱符号时,随机选择一个;
静态链接器主要缺陷就是:当库需要更新时,要重新编译整个目标文件;其次多个函数包含相同的静态库时,造成对内存的极大浪费。因此引入动态编译(库)。
在动态库的情况下,有两个文件,一个是引入库(.LIB)文件,一个是DLL文件,引入库文件包含被DLL导出的函数的名称和位置,DLL包含实际的函数和数据,应用程序使用LIB文件链接到所需要使用的DLL文件,库中的函数和数据并不复制到可执行文件中,因此在应用程序的可执行文件中,存放的不是被调用的函数代码,而是DLL中所要调用的函数的内存地址,这样当一个或多个应用程序运行时再把程序代码和被调用的函数代码链接起来,从而节省了内存资源。

46、 继承和组合的区别

用组合实现重用,用继承实现多态
都能够实现代码的复用,继承和组合都能从现有类型生成新类型。组合一般是将现有类型作为新类型底层实现的一部分来加以复用,而继承复用的是接口。
组合是has a的关系;继承是is a的关系;
继承缺点:父类的实现细节暴露给子类,破坏了封装性。当父类的实现代码修改时,可能使得子类也不得不修改,增加维护难度。耦合度高,不支持动态扩展。
组合的缺点:创建整体类对象时,需要创建所有局部类对象。导致系统对象很多。
除非考虑使用多态,否则优先使用组合。

47、 在成员函数里delete this会怎么样(对象在栈上,在堆上)

在类对象的内存空间中,只有数据成员和虚函数表指针,并不包含代码内容,类的成员函数单独放在代码段中。
当调用delete this时,类对象的内存空间被释放。在delete this之后进行的其他任何函数调用,只要不涉及到this指针的内容,都能够正常运行。一旦涉及到this指针,如操作数据成员,调用虚函数等,就会出现不可预期的问题。
在析构函数中调用delete this,delete的本质是“为将被释放的内存调用一个或多个析构函数,然后,释放内存”。显然,delete this会去调用本对象的析构函数,而析构函数中又调用delete this,形成无限递归,造成堆栈溢出,系统崩溃。
而在实际的运行过程中使用delele this确实会直接出现错误。这是因为:在成员函数中调用delete this,首先会调用类的析构函数,this指针已删除,会出现指针错误。
总结:在成员函数中调用delete this,会导致指针错误,而在析构函数中调用delete this,出导致死循环,造成堆栈溢出。
https://blog.csdn.net/yp18792574062/article/details/73865370

48、 内存泄漏,举几个例子

1)在类的构造函数和析构函数中没有匹配的调用new和delete函数
2)在释放对象数组时在delete中没有使用方括号
3)没有将基类的析构函数定义为虚函数
4)缺少赋值运算符,旧指针指向的内容未被释放(深拷贝和浅拷贝);

49、 为什么引入抽象基类和纯虚函数

含有纯虚函数的基类叫做抽象基类;抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中,由它来为派生类提供一个公共的根,派生类将具体实现在其基类中作为接口的操作。抽象类,不能生成对象,只能派生。
引入的原因:1)为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。2)在很多情况下,基类本身生成对象是不合理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合理。为了解决上述问题,引入纯虚函数的概念,将函数定义为纯虚函数,则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。

50、 为什么传指针比传引用安全

(1) 引用在创建的同时必须初始化,即引用到一个有效的对象;而指针在定义的时候不必初始化,可以在定义后面的任何地方重新赋值。
(2) 不存在NULL引用,引用必须与合法的存储单元关联;而指针则可以是NULL。
(3) 引用一旦被初始化为指向一个对象,它就不能被改变为另一个对象的引用;而指针在任何时候都可以改变为指向另一个对象.给引用赋值并不是改变它和原始对象的绑定关系。
(4) 引用的创建和销毁并不会调用类的拷贝构造函数。

51、 函数指针的定义

如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。
函数返回值类型 (* 指针变量名) (函数参数列表);
http://c.biancheng.net/view/228.html

52、 STL相关容器的底层实现

vector:连续的一段存储空间,预分配空间,用三个指针分别指向起始地址,当前最后一个元素的地址和分配空间的尾地址。注意内存自动扩充和转移。
deque:双端队列,允许在头尾进行插入删除操作。动态地以分段连续空间组合而成,随时可以增加一段新的空间并链接起来。因此没有容量的概念。可以随机访问,但是效率小于vector。deque可以理解成多个vector链接起来的一个结构,由一个指针数组存放指向各个的起始地址。
(https://blog.csdn.net/u012940886/article/details/80529721)。
list:底层是一个双向的环状链表,只需要一个指针就可以完整的表现整个链表,只需要刻意的在尾部加一个空白链表就可以表示前闭后开,支持随机插入不支持随机访问;
forward_list:前向链表,底层是单链表;
stack:用list作为底层容器,没有迭代器;
queue:用deque作为底层容器,封闭底端的出口和前端的入口,不能遍历没有迭代器;
set:不允许有两个相同的元素,底层用红黑树实现。set的键值就是实值,实值就是键值,不存在键值对。set有一组特别的算法交集(set_intersection),并集(set_unio),差集(set_difference)。
map:所有元素都会根据元素的键值进行排序。不允许有两个元素具有相同的键值。底层也是红黑树实现的。
multiset和multimap:底层也都是红黑树,与set和map不同点在于允许有重复的元素。红黑树插入采用的是insert_equal()而不是insert_unique()。
hashtable, hash_set, hash_map,hash_multiset和hash_multimap:和上面介绍的容器最大区别在于他们的元素不会被自动排序。并且访问,插入和删除元素的时间复杂度都是常数级别的。

53、 智能指针是否支持引用传递,值传递?支持数组吗?

这里的智能指针我们主要讨论两个:shared_ptr和unique_ptr;
对于shared_ptr可以传引用和传值,值得注意的是当传值的时候use_count()至少为2,调用了复制构造函数;
对于unique_ptr只能传递引用(需要进一步核实,编译器实验结果);
两个都支持定义数组的类型;

54、 构造函数必须初始化什么(初始化列表)

1)成员类型是没有默认构造函数的类。若没有提供显示初始化式,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。
2)const成员或引用类型的成员。因为const对象或引用类型只能初始化,不能对他们赋值。(第一次读有点费解,在函数定义const或者引用不初始化编译器就会报错。但是在测试后发现,在类中可以定义而且不初始化(长见识了))。

55、 初始化列表进行对象初始化注意事项:

1) 初始化的顺序不是初始化列表的顺序,而是变量定义的顺序;
2) 用初始化列表初始化和在函数体内部进行初始化有什么不同:两个方面,如果变量是内置类型或者指针等,没有什么区别。但是如果变量是自定义类类型或者是结构体而且具有较大的内存空间,效率上会存在较大的不同。在初始化列表中直接调用基类的构造函数而在函数体中涉及到调用复制构造函数和析构函数等操作。

56、 vector使用迭代器删除偶数

这一题考察的是erase()函数返回值为被删除元素的后面一个元素。

57、 C++中volatile关键字的作用

Volatile关键字主要是在多线程中保持数据的一致性而产生的一种类型。
由于CPU执行速度与内存的读取速度存在较大的差异限制了线程的执行效率,因此引入了高速缓存机制。CPU将程序运行的一些变量暂时存储在高速缓存中,因此多核处理器就可能会出现数据不一致的情况。该类型变量能够让命名执行完毕之后立刻将变量值刷新回内存中,并且下次读取只会在内存中读取,就保证了数据的一致性。
此外,改变量还会确保程序不会被编译器优化而导致的可能的问题。编译器优化主要涉及到对一些语句顺序的调换和一些变量的优化。
https://blog.csdn.net/m0_37506254/article/details/81408781

58、 C++中为什么可以通过指针或引用实现多态,而不可以通过对象?

从函数的底层上解释:对于含有虚函数的类,只有唯一的一个虚表存在于内存空间中。不管定义多少个类的对象,都是通过对象内存空间中的虚指针Ptr指向虚表。对象的内存空间中排布首先是虚标指针,然后是类的非静态成员变量。成员函数定义在内存空间的其他位置。因此通过指针指向的对象,实际上是指向的对象内存空间的首地址(虚表指针)。因此可以通过虚表指针和虚表实现函数的多态。而对于对象类型,不管是通过赋值还是复制构造函数都只会更改成员变量的大小,不会更改虚表指针,因此无法实现动态的多态。引用可以看成是指针的解引操作,原理同指针可以实现多态。
https://www.cnblogs.com/yinheyi/p/10525543.html

你可能感兴趣的文章
利用负载均衡优化和加速HTTP应用
查看>>
消息队列设计精要
查看>>
分布式缓存负载均衡负载均衡的缓存处理:虚拟节点对一致性hash的改进
查看>>
分布式存储系统设计(1)—— 系统架构
查看>>
MySQL数据库的高可用方案总结
查看>>
常用排序算法总结(一) 比较算法总结
查看>>
剖析 Linux hypervisor
查看>>
SSH原理与运用
查看>>
程序员之深刻的思辨和严密的体系结构
查看>>
黄威地址的openeim001
查看>>
工人的工资少openeim002
查看>>
ye我们胜利了的shooow
查看>>
太白山可真雄伟的shooow
查看>>
只见他满身尘土的openeim
查看>>
故事从一只平凡的openeim002
查看>>
真是哑巴吃黄连的openeim001
查看>>
MainActivity 会异步加载图片到相应的ImageView上
查看>>
妈妈十分生气的shooow
查看>>
怎么写一个温泉管理系统
查看>>
令人神清气爽的shooow
查看>>