C++面试问题整理汇总

一. 封装、继承和多态

1. 面向对象的三大特性是什么?

封装:将数据和实现操作的代码集中放在C++类的内部,并尽可能隐蔽类的内部实现细节,实现了代码模块化,如类里面的private和protect;
继承:使得子类可以复用父类的数据成员和方法,实现了代码重用;
多态:是一个接口多种实现状态。多态包括编译时多态和运行时多态,编译时多态(静态多态)主要包含函数重载与模板,运行时多态(动态多态)是由继承及虚函数实现的动态绑定。

2. 多态的理解

C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类类,别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数

多态的实现主要分为静态多态和动态多态,静态多态主要是重载,在编译的时候就已经确定;动态多态是用虚函数机制实现的,在运行期间动态绑定

多态的条件:

1.必须有继承

2.要有虚函数重写

3.用父类指针(引用)指向子类对象

二. 指针和引用

1. 指针数组和数组指针的区别

数组指针,是指向数组的指针,而指针数组则是指该数组的元素均为指针。

数组指针,是指向数组的指针,其本质为指针,形式如下。如 int (*p)[10],p即为指向数组的指针,()优先级高,首先说明p是一个指针,指向一个整型的一维数组,这个一维数组的长度是n,也可以说是p的步长。也就是说执行p+1时,p要跨过n个整型数据的长度。数组指针是指向数组首元素的地址的指针,其本质为指针,可以看成是二级指针。

类型名 (*数组标识符)[数组长度]

指针数组,在C语言和C++中,数组元素全为指针的数组称为指针数组,其中一维指针数组的定义形式如下。指针数组中每一个元素均为指针,其本质为数组。如 int *p[n], []优先级高,先与p结合成为一个数组,再由int*说明这是一个整型指针数组,它有n个指针类型的数组元素。这里执行p+1时,则p指向下一个数组元素,这样赋值是错误的:p=a;因为p是个不可知的表示,只存在p[0]、p[1]、p[2]…p[n-1],而且它们分别是指针变量可以用来存放变量地址。但可以这样 *p=a; 这里*p表示指针数组第一个元素的值,a的首地址的值。

类型名 *数组标识符[数组长度]

三. 内存相关

1. C++程序内存的五大分区是哪五个?每个分区用来存放什么样的内容?

在C++中,内存分成5个区,他们分别是堆、栈、全局/静态存储区、常量存储区和代码区

1)栈区,当代码运行进函数时,函数内的局部变量的内存是在在栈上分配的,函数执行结束时这些栈内存会自动被释放。此外,函数调用时的传递的参数,是通过栈内存传递给被调用函数的,这点查看汇编代码一目了然。

2)堆区,通过malloc或new动态申请的内存都是在堆上分配的,这些堆内存需要程序员自行去管理,调用free或delete去释放。如果代码中不释放,则在程序结束运行时由操作系统去释放。

3)全局/静态存储区,它主要存放静态数据(局部static变量,全局static变量)和全局变量,这些变量的内存在程序启动进入main函数之前就分配好了,这块内存在程序的整个运行期间都存在,在程序退出时释放。

4)常量存储区,这是一块比较特殊的存储区,他们里面存放的是常量字符串,不允许修改。

5)代码区,存放程序的二进制代码。exe程序启动时系统会将exe程序依赖的所有二进制文件及exe文件加载到进程空间中,这些二进制文件中存放的是二进制代码,这些二进制代码占用的内存,是代码段内存,是从进程的虚拟内存空间划拨的。

2. 堆和栈的区别?

1).堆存放动态分配的对象——即那些在程序运行时动态分配的对象,比如 new 出来的对象,其生存期由程序控制;

2).栈用来保存定义在函数内的非static对象,如局部变量,仅在其定义的程序块运行时才存在;

3).静态内存用来保存static对象,类static数据成员以及定义在任何函数外部的变量,static对象在使用之前分配,程序结束时销毁;

4).栈和静态内存的对象由编译器自动创建和销毁。

3. 什么是内存泄漏?面对内存泄漏和指针越界,你有哪些方法?你通常采用哪些方法来避免和减少这类错误?

用动态存储分配函数动态开辟的空间,在使用完毕后未释放,结果导致一直占据该内存单元即为内存泄露。比如:

  • 有些内存资源已经被释放,但指向它的指针并没有改变指向(成为了野指针),并且后续还在使用;
  • 有些内存资源已经被释放,后期又试图再释放一次(重复释放同一块内存会导致程序运行崩溃);
  • 没有及时释放不再使用的内存资源,造成内存泄漏,程序占用的内存资源越来越多。

在C++中应该优先考虑使用智能指针

4. 智能指针实现原理

智能指针的本质是使用栈上的智能指针对象来管理堆上申请的内存空间,当栈上的智能指针对象销毁时,管理的堆上的对象也会随之释放,不需要手动delete

5. 共享指针shared_ptr的理解与应用

shared_ptr 是存储动态创建对象的指针,其主要功能是管理动态创建对象的销毁,从而帮助彻底消除内存泄漏和悬空指针的问题。

基本原理:就是记录对象被引用的次数,当引用次数为 0 的时候,也就是最后一个指向该对象的共享指针析构的时候,共享指针的析构函数就把指向的内存区域释放掉。
特点:它所指向的资源具有共享性,即多个shared_ptr可以指向同一份资源,并在内部使用引用计数机制来实现这一点。

共享指针内存:每个 shared_ptr 对象在内部指向两个内存位置:

    指向对象的指针;
    用于控制引用计数数据的指针。

1.当新的 shared_ptr 对象与指针关联时,则在其构造函数中,将与此指针关联的引用计数增加1。
2.当任何 shared_ptr 对象超出作用域时,则在其析构函数中,它将关联指针的引用计数减1。如果引用计数变为0,则表示没有其他 shared_ptr 对象与此内存关联,在这种情况下,它使用delete函数删除该内存。
文章分享:

C++:共享指针shared_ptr的理解与应用_不会编程的-程序猿的博客-CSDN博客

四. class类

1. struct结构体和class类的区别是什么?

1)使用struct时,它的成员的默认访问权限是public,而class的成员默认是private的;
2)struct的继承默认是public继承,而class的继承默认是private继承;
3)class可以使用模板,而struct不能。

2.什么时候生成默认构造函数(无参构造函数)?什么时候生成默认拷贝构造函数?什么是深拷贝?什么是浅拷贝?默认拷贝构造函数是哪种拷贝?什么时候用深拷贝?

没有任何构造函数时,编译器会自动生成默认构造函数,也就是无参构造函数;当类没有拷贝构造函数时,会生成默认拷贝构造函数

深拷贝和浅拷贝这两个概念是在项目中比较常见的,在很多时候,都会遇到拷贝的问题,我们总是需要将一个对象赋值到另一个对象上,但可能会在改变新赋值对象的时候,忽略掉我是否之后还需要用到原来的对象,那么就会出现当改变新赋值对象的某一个属性时,也同时改变了原对象,此时我们就需要用到拷贝这个概念了

深拷贝和浅拷贝的区别

1.浅拷贝: 将原对象或原数组的引用直接赋给新对象,新数组,新对象/数组只是原对象的一个引用

2.深拷贝: 创建一个新的对象和数组,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是“引用”

3.虚析构函数的作用?

基类采用虚析构函数可以防止内存泄露

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

4. C++中重写和重载的区别

1.重载(overload):函数名相同,函数的参数个数,参数类型或者参数顺序三者中必须至少有一种不同。函数返回值的类型可以相同,也可以不同。发生在一个类内部,不能跨作用域。也就是说使用同一个函数去完成不同的功能。

2.重写(override):也叫做覆盖,一般发生在子类和父类继承关系之间。子类重新定义父类中有相同名称和参数的虚函数,返回值可以不相同,但是必须是父子关系的指针或者引用。

重写需要注意:

a.被重写的函数不能是static,必须是virtual

b.重写函数必须有相同的类型,名称和参数列表

c.重写函数的访问修饰符(public/private/protected)可以不同

五. 关键字相关

1. C++中const的使用场景有哪些?

1)const可以用来修饰变量,变量值是不可修改的

2)可以用来修饰函数的参数,函数中不得修改变量的值

3)可以用来修饰函数返回值的类型,返回值是不可修改的

4)可以用来修饰类的成员函数,则该函数不能修改类成员变量的值

2. nline内联函数与宏的异同点有哪些? 

函数调用时会有调用的开销,比如参数的入栈出栈操作、保护线程和恢复现场操作等,所以引入了内联函数,避免了函数调用的开销。内联函数实现不宜过长,一般不超过10句代码

共同点:在编译时编译器会直接将它们的实现部分直接替换到调用处

区别:

1)内联函数在编译时展开,而宏在预编译时展开。
2)在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。
3)内联函数可以进行诸如类型安全检查、语句是否正确等编译功能,宏不具有这样的功能。
4)宏不是函数,而inline作用的是函数。
5)宏在定义时要小心处理宏参数,一般用括号括起来,否则容易出现二义性,而内联函数不会出现二义性。
6)inline可以不展开,宏一定要展开。因为inline指示对编译器来说,只是一个建议,编译器可以选择忽略该建议(比如编译器发现函数中有循环等操作时会自动忽略内联关键字),不对该函数进行展开。

六. STL标准模板库

1. 常用的STL容器有哪些?容器中有哪三个重要的概念?

STL中常用的容器有vector、list、map、deque、set、queue等
STL中有三种概念:容器、迭代器和算法函数

2. 算法函数你知道吗?如何提高stl容器的遍历效率?

常用的算法函数有sort、count、count_if、find、find_if、remove_copy和remove_copy_if等。这些算法函数比使用迭代器去for循环遍历容器要快很多,在数据量比较大的情况下效率比较高,可以有效地提高STL容器的遍历效率。

3. 哪两种容器中的数据是有序存放数据的?有什么好处?

set和map中存放的数据是有序的,数据的有序排列有利于提升查找的效率

4. vector和数组有什么区别?

vector和数组类似,拥有一段连续的内存空间。vector申请的是一段连续的内存,当插入新的元素内存不够时,通常以2倍重新申请更大的一块内存,将原来的元素拷贝过去,释放旧空间。因为内存空间是连续的,所以在进行插入和删除操作时,会造成内存块的拷贝,时间复杂度为o(n)。
STL中的vector是封装了动态数组的顺序容器。不过与动态数组不同的是,vector可以根据需要自动扩大容器的大小。具体策略是每次容量不够用时重新申请一块大小为原来容量两倍的内存,将原容器的元素拷贝至新容器,并释放原空间,返回新空间的指针。

5. vector和list有什么区别?

1)vector使用的是一段连续的内存,类似于数组,可以像数组一样使用下标访问列表中的元素;list使用的内存不是连续的,是用双向链表实现的。
2)vector支持使用下标的方式去随机访问元素,list不支持。
3)vector在中间节点进行插入和删除元素时会触发内存拷贝,list不会。
4)vector一次性分配好内存,不够时才进行2倍扩容;list每次插入新节点都会进行内存申请。
5)vector随机访问性能好,插入删除性能差;list随机访问性能差,插入删除性能好。

6. map和unordered_map区别和联系以及map的使用

在c++中有两个关联容器,一个是map,另一个是unordered_map。下面说一下他们之间内部实现机理。

一、map和unordered_map的实现机理:
map:是基于红黑树来实现的(红黑树是非常严格的平衡二叉搜索树),红黑树具有自动排序功能,红黑树的每一个节点都代表着map中的一个元素,因此对于map的查找,删除和插入操作都是对红黑树的操作。
unordered_map:是基于哈希表来实现的,查找的时间复杂度是O(1),在海量数据处理中有着广泛的应用。

二、map和unordered_map的优缺点
map的优点:(1)map是有序的(2)基于红黑树实现,查找的时间复杂度是O(n)
map的缺点:空间占用率比较高,因为内部实现了红黑树,虽然提高了运行效率,但是每个节点都要保存父亲节点和孩子节点和红黑树的性质,使得每一个节点都占用大量的空间。
适用的情况:对于要有序的结构,适用map
unordered_map的优点:因为内部是哈希表来实现的,所以查找效率会非常高
unordered_map的缺点:哈希表的建立比较费时
适用的情况:对于查找问题,适用unordered_map会更好一点。

map的使用

/*map中常用的操作
*begin()	还回指向map头部的迭代器
*clear()	删除所有元素,注意是所有元素
*count()	还回指定元素出现的次序
*empty()	如果map为空则还回true
*end()		还回指向map末尾的迭代器
*erase()	删除一个元素
*find()		查找一个元素
*insert()	插入一个元素
*max_size()	还回可以容纳的最大元素个数
*size()		还回map中元素的个数
*swap()		交换两个map
*/

int main() {
	map<int, char> m;
	//一、数据的插入
	m.insert(pair<int, char>(1, 'a'));
	m.insert(pair<int, char>(3, 'b'));
	m.insert(pair<int, char>(2, 'c'));
	m.insert(pair<int, char>(-1, 'd'));
	map<int, char>::iterator it = m.begin();
	for (; it != m.end(); it++) {
		cout << it->first << ":" << it->second << endl;
	}
	//二、数据的查找
	/*(1)使用find()函数,该函数可以找到key对应的value
	(2)使用count()函数,该函数的还回值只有0和1,1为找到,但是还回要查找的值*/
	it = m.find(1);
	if (it != m.end()) { cout << "find" << it->second << endl; }
	else { cout << "not find" << endl; }

	//三、map的清空
	//m.clear(m.begin(), m.end());

	//四、数据的删除
	//m.erase(it);

	//五、map的反向遍历,使用反向迭代容器
	
	for (map<int, char>::reverse_iterator Rit = m.rbegin(); Rit!=m.rend(); Rit++) {
		cout << Rit->first << ":" << Rit->second;
	}
	return 0;
}

七. C++11特性

1. 了解C++11标准中的内容吗?都知道哪些C++11特性?

1)自动类型推导auto:auto的自动类型推导用于从初始化表达式中推断出变量的数据类型。通过auto的自动类型推导,可以大大简化我们的编程工作。
2)nullptr:nullptr是为了解决原来C++中NULL的二义性问题而引进的一种新的类型,因为NULL实际上代表的是0,而nullptr是void*类型的。
3)lambda表达式:它类似Javascript中的闭包,它可以用于创建并定义匿名的函数对象,很好地简化了函数编程的工作。
4)智能指针auto_ptr、unique_ptr、shared_ptr和weak_ptr,智能指针在开源项目中使用的比较多。
5)override关键字,显示的声明重写了父类的方法。

八. 设计模式

1. 单例模式

实现单实例时,需要将构造函数设置为私有的,外部不得使用类去定义类对象,必须调用获取实例对象的接口。定义单实例时可以使用static静态对象的方式,也可以去动态去new一个对象,然后将对象保存到static静态指针变量中。

九. 其他

1. #include <filename.h>和#include “filename.h”有什么区别?

这两种方式与编译器编译时搜索头文件的路径有关系

对于#include <filename.h> ,编译器从标准库路径开始搜索filename.h
对于#include “filename.h” ,编译器从用户的工作路径开始搜索filename.h

2. 左值和右值

可以取地址的,有名字的,非临时的就是左值

不能取地址的,没有名字的,临时的,通常生命周期就在某个表达式之内的就是右

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值