c++面试常见问题汇总与解析
-
- 1.指针和引用的区别
- 2.堆和栈的区别
- 3.new和delete是如何实现的,new 与 malloc的异同处
- 4.C和C++的区别
- 5.C++、Java的联系与区别,包括语言特性、垃圾回收、应用场景等(java的垃圾回收机制)
- 6.Struct和class的区别
- 7. define 和const的区别(编译阶段、安全性、内存占用等)
- 8. 在C++中const和static的用法(定义,用途)
- 9. C++中的const类成员函数(用法和意义),以及和非const成员函数的区别
- 10. C++的顶层const和底层const
- 11. final和override关键字
- 12. 拷贝初始化和直接初始化
- 13. 初始化和赋值的区别
- 14. extern "C"的用法
- 15. 模板函数和模板类的特例化
- 16. C++的STL源码(这个系列也很重要,建议侯捷老师的STL源码剖析书籍与视频),其中包括内存池机制,各种容器的底层实现机制,算法的实现原理等)
- 17. STL源码中的hashtable的实现
- 18. STL中unordered_map和map的区别和应用场景
- 19. STL中vector的实现
- 20. STL容器的几种迭代器以及对应的容器(输入迭代器,输出迭代器,前向迭代器,双向迭代器,随机访问迭代器)
- 21. STL中的traits技法
- 22. vector使用的注意点及其原因,频繁对vector调用push_back()对性能的影响和原因。
- 23. C++中的重载和重写的区别
- 24. C++内存管理,内存池技术(热门问题),与csapp中几种内存分配方式对比学习加深理解
- 25. 介绍面向对象的三大特性,并且举例说明每一个
- 26. C++多态的实现
- 27. C++虚函数相关(虚函数表,虚函数指针),虚函数的实现原理(包括单一继承,多重继承等)(拓展问题:为什么基类指针指向派生类对象时可以调用派生类成员函数,基类的虚函数存放在内存的什么区,虚函数表指针vptr的初始化时间)
- 28. C++中类的数据成员和成员函数内存分布情况
- 29. this指针
- 30. 析构函数一般写成虚函数的原因
- 31. 构造函数、拷贝构造函数和赋值操作符的区别
- 32. 构造函数声明为explicit
- 33. 构造函数为什么一般不定义为虚函数
- 34. 构造函数的几种关键字(default delete 0)
- 35. 构造函数或者析构函数中调用虚函数会怎样
- 36. 纯虚函数
- 37. 静态类型和动态类型,静态绑定和动态绑定的介绍
- 38. 引用是否能实现动态绑定,为什么引用可以实现
- 39. 深拷贝和浅拷贝的区别(举例说明深拷贝的安全性)
- 40. 对象复用的了解,零拷贝的了解
- 41. 介绍C++所有的构造函数
- 42. 什么情况下会调用拷贝构造函数(三种情况)
- 43. 结构体内存对齐方式和为什么要进行内存对齐?
- 44. 内存泄露的定义,如何检测与避免?
- 45. 手写智能指针的实现(shared_ptr和weak_ptr实现的区别)
- 46. 智能指针的循环引用
- 47. 遇到coredump要怎么调试
- 48. 内存检查工具的了解
- 49. 模板的用法与适用场景
- 50. 成员初始化列表的概念,为什么用成员初始化列表会快一些(性能优势)?
- 51. 用过C++ 11吗,知道C++ 11哪些新特性?
- 52. C++的调用惯例(简单一点C++函数调用的压栈过程)
- 53. C++的四种强制转换
- 54. C++中将临时变量作为返回值的时候的处理过程(栈上的内存分配、拷贝过程)
- 55. C++的异常处理
- 56. volatile关键字
- 57. 优化程序的几种方法
- 58. public,protected和private访问权限和继承
- 60. decltype()和auto
- 61. inline和宏定义的区别
- 62. C++和C的类型安全
- 63. 参考链接
1.指针和引用的区别
(1)指针有自己的一块空间,而引用只是一个别名;
(2)使用 sizeof 看一个指针的大小为4字节(32位,如果要是64位的话指针为8字节),而引用则是被引用对象的大小。
(3)指针可以被初始化为 NULL,而引用必须被初始化且必须是一个已有对象的引用。
(4)作为参数传递时,指针需要被解引用才可以对对象进行操作,而直接对引用的修改都会改变引用所指向的对象。
(5)指针在使用中可以指向其他对象,但是引用只能是一个对象的引用,不能被改变。
(6)指针可以是多级,而引用没有分级
(7)如果返回动态分配内存的对象或者内存,必须使用指针,引用可能引起内存泄漏。
2.堆和栈的区别
(1)堆栈空间分配区别:
- 栈(操作系统):由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈;
- 堆(操作系统): 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收,分配方式倒是类似于链表。
(2)堆栈的缓存方式区别
- 栈:是内存中存储值类型的,大小为2M(window,linux下默认为8M,可以更改),超出则会报错,内存溢出
- 堆:内存中,存储的是引用数据类型,引用数据类型无法确定大小,堆实际上是一个在内存中使用到内存中零散空间的链表结构的存储空间,堆的大小由引用类型的大小直接决定,引用类型的大小的变化直接影响到堆的变化
(3)堆栈数据结构上的区别
堆(数据结构):堆可以被看成是一棵树,如:堆排序;
栈(数据结构):一种先进后出的数据结构。
3.new和delete是如何实现的,new 与 malloc的异同处
3.1. new操作针对数据类型的处理,分为两种情况:
(1) 简单数据类型(包括基本数据类型和不需要构造函数的类型)
- 简单类型直接调用 operator new 分配内存;
- 可以通过new_handler 来处理 new 失败的情况;
- new 分配失败的时候不像 malloc 那样返回
NULL,它直接抛出异常(bad_alloc)。要判断是否分配成功应该用异常捕获的机制;
(2)复杂数据类型(需要由构造函数初始化对象)
- new 复杂数据类型的时候先调用operator new,然后在分配的内存上调用构造函数。
3.2. delete也分为两种情况:
(1) 简单数据类型(包括基本数据类型和不需要析构函数的类型)
- delete简单数据类型默认只是调用free函数。
(2)复杂数据类型(需要由析构函数销毁对象)
- delete复杂数据类型先调用析构函数再调用operator delete。 从原理上来分析可以看看这篇博客:C++
new和delete的实现原理
3.3. new和delete与 malloc 和 free 的区别:
(1)属性上:new / delete 是c++关键字,需要编译器支持。 malloc/free是库函数,需要c的头文件支持。
(2)参数:使用new操作符申请内存分配时无须制定内存块的大小,编译器会根据类型信息自行计算。而mallco则需要显式地指出所需内存的尺寸。
(3)返回类型:new操作符内存分配成功时,返回的是对象类型的指针,类型严格与对象匹配,故new是符合类型安全性的操作符。而malloc内存成功分配返回的是void *,需要通过类型转换将其转换为我们需要的类型。
(4)分配失败时:new内存分配失败时抛出bad_alloc异常;malloc分配内存失败时返回 NULL。
(5)自定义类型:new会先调用operator new函数,申请足够的内存(通常底层使用malloc实现)。然后调用类型的构造函数,初始化成员变量,最后返回自定义类型指针。delete先调用析构函数,然后调用operator delete函数释放内存(通常底层使用free实现)。 malloc/free是库函数,只能动态的申请和释放内存,无法强制要求其做自定义类型对象构造和析构工作。
(6)重载:C++允许重载 new/delete 操作符。而malloc为库函数不允许重载。
(7)内存区域:new操作符从自由存储区(free store)上为对象动态分配内存空间,而malloc函数从堆上动态分配内存。其中自由存储区为:C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。而堆是操作系统中的术语,是操作系统所维护的一块特殊内存,用于程序的内存动态分配,C语言使用malloc从堆上分配内存,使用free释放已分配的对应内存。自由存储区不等于堆,如上所述,布局new就可以不位于堆中。
4.C和C++的区别
C 是面向过程的一门编程语言,C++ 可以很好地进行面向对象的程序设计。C++ 虽然主要是以 C 的基础发展起来的一门新语言,但它不是 C 的替代品,它们是兄弟关系。面向对象和面向过程不是矛盾的,而是各有用途、互为补充的。
C++ 对 C 的增强,表现在六个方面:
- 增强了类型检查机制
- 增加了面向对象的机制
- 增加了泛型编程的机制(template)
- 增加了异常处理
- 增加了重载的机制
增加了标准模板库(STL)
(1)类型检查
C/C++ 是静态数据类型语言,类型检查发生在编译时,因此编译器知道程序中每一个变量对应的数据类型。C++ 的类型检查相对更严格一些。
很多时候需要一种能够实际表示多种类型的数据类型。传统上 C 使用 void* 指针指向不同对象,使用时强制转换回原始类型或兼容类型。这样做的缺陷是绕过了编译器的类型检查,如果错误转换了类型并使用,会造成程序崩溃等严重问题。
C++ 通过使用基类指针或引用来代替 void* 的使用,避免了这个问题(其实也是体现了类继承的多态性)。
面向对象
C 的结构体传递的是一种数据结构,我们只是在主函数里面对这种数据类型做某种调用。主函数的架构依然是基于函数、函数族的处理过程,即面向过程。
C++ 中最大的区别就是允许在结构体中封装函数,而在其他的地方直接调用这个函数。这个封装好的可直接调用的模块有个新名词——对象;并且也把结构体换一个名字——类。这就是面向对象的思想。在构建对象的时候,把对象的一些操作全部定义好并且给出接口的方式,对于外部使用者而言,可以不需要知道函数的处理过程,只需要知道调用方式、传递参数、返回值、处理结果。
泛型编程(template)
所谓泛型编程,简而言之就是不同的类型采用相同的方式来操作。在 C++ 的使用过程中,直接 template 用的不多,但是用 template 写的库是不可能不用的。因此需要对泛型有比较深入的了解,才可以更好地使用这些库。
C++ 里面的模版技术具有比类、函数更高的抽象水平,因为模版能够生成出(实例化)类和函数。可以用来:
异常处理
C 语言不提供对错误处理的直接支持,但它以返回值的形式允许程序员访问底层数据。在发生错误时,大多数的 C 或 UNIX 函数调用返回 1 或 NULL,同时会设置一个错误代码 errno,该错误代码是全局变量,表示在函数调用期间发生了错误。可以在 errno.h 头文件中找到各种各样的错误代码。
所以,C 程序员可以通过检查返回值,然后根据返回值决定采取哪种适当的动作。开发人员应该在程序初始化时,把 errno 设置为 0(表示没有错误),这是一种良好的编程习惯。
C++ 提供了一系列标准的异常,定义在 中,我们可以在程序中使用这些标准的异常。
函数重载 & 运算符重载
C++ 可以实现函数重载,条件是:函数名必须相同,返回值类型也必须相同,但参数的个数、类型或顺序至少有其一不同。
重载的运算符是带有特殊名称的函数,函数名是由关键字 operator 和其后要重载的运算符符号构成的。大多数的重载运算符可被定义为普通的非成员函数(func(a, b) 形式调用)或者被定义为类成员函数(a.func(b) 形式调用)。
标准模板库(STL)
5.C++、Java的联系与区别,包括语言特性、垃圾回收、应用场景等(java的垃圾回收机制)
通常,我们聊到Java,第一印象“面向对象”,“没有指针,编写效率高,执行效率较低”。更深入、专业一点就谈论 “java内存自动回收(GC垃圾回收机制),多线程编程”。**
java的三大特性是封装、继承和多态。**
总结如下:
(1) JAVA的应用在高层,C++在中间件和底层
(2) JAVA离不开业务逻辑,而C++可以离开业务为JAVA们服务
(3) java语言给开发人员提供了更为简洁的语法;取消了指针带来更高的代码质量;完全面向对象,独特的运行机制是其具有天然的可移植性。
(4) java 是运行在JVM上的,之所以说它的可移植性强,是因为jvm可以安装到任何的系统
(5) c++不是不能在其他系统运行,而是c++在不同的系统上运行,需要不同的编码(这一点不如java,只编写一次代码,到处运行)。java程序一般都是生成字节码,在JVM里面运行得到结果。
(6) java 在web 应用上具有c++ 无可比拟的优势
(7) java在桌面程序上不如c++实用,C++可以直接编译成exe文件,指针是c++的优势,可以直接对内存的操作,但同时具有危险性 。(操作内存的确是一项非常危险的事情,一旦指针指向的位置发生错误,或者误删除了内存中某个地址单元存放的重要数据,后果是可想而知的)。
(8) 垃圾回收机制的区别。c++用析构函数回收垃圾,java自动回收(GC算法),写C和C++程序时一定要注意内存的申请和释放。
(9) java 丰富的插件是java 发展如此迅速的原因
(10)java 很大的沿袭了c++的一些实用结构
(11)对于底层程序的编程以及控制方面的编程,c++很灵活,因为有句柄的存在。Java并不仅仅是C++语言的一个变种,它们在某些本质问题上有根本的不同:
- Java比C++程序可靠性更高。有人曾估计每50行C++程序中至少有一个BUG。姑且不去讨论这个数字是否夸张,但是任何一个C++程序员都不得不承认C++语言在提供强大的功能的同时也提高了程序含BUG的可能性。Java语言通过改变语言的特性大大提高了程序的可靠性。
- Java语言不需要程序对内存进行分配和回收。Java丢弃了C++
中很少使用的、很难理解的、令人迷惑的那些特性,如操作符重载、多继承、自动的强制类型转换。特别地,Java语言不使用指针,并提供了自动的废料收集,在Java语言中,内存的分配和回收都是自动进行的,程序员无须考虑内存碎片的问题。 - Java语言中没有指针的概念,引入了真正的数组。不同于C++中利用指针实现的“伪数组”,Java引入了真正的数组,同时将容易造成麻烦的指针从语言中去掉,这将有利于防止在c++程序中常见的因为数组操作越界等指针操作而对系统数据进行非法读写带来的不安全问题。
- Java用接口(Interface)技术取代C++程序中的多继承性。接口与多继承有同样的功能,但是省却了多继承在实现和维护上的复杂性。
6.Struct和class的区别
(1)首先说一下C中的结构体和C++中的结构体的异同:
(2)C++中 struct 与 class 的区别:
- 内部成员变量及成员函数的默认访问属性:struct 默认防控属性是 public 的,而 class 默认的访问属性是private的
- 继承关系中默认访问属性的区别:在继承关系,struct 默认是 public 的,而 class 是 private
- class这个关键字还可用于定义模板参数,就等同于 typename;而strcut不用与定义模板参数
7. define 和const的区别(编译阶段、安全性、内存占用等)
(1)起作用的阶段: #define是在编译的预处理阶段起作用,而const是在编译、运行的时候起作用。
(2)作用的方式:const常量有数据类型,而宏常量没有数据类型,只是简单的字符串替换。编译器可以对前者进行类型安全检查。而对后者没有类型安全检查,并且在字符替换时可能会产生意料不到的错误。
(3)存储的方式:#define只是进行展开,有多少地方使用,就替换多少次,它定义的宏常量在内存中有若干个备份;const定义的只读变量在程序运行过程中只有一份备份,const比较节省空间,避免不必要的内存分配,提高效率。
8. 在C++中const和static的用法(定义,用途)
(1)static:
- 修饰全局变量:存储在静态存储区;未经初始化的全局静态变量自动初始化为 0;作用域为整个文件之内。
- 修饰局部变量:存储在静态存储;未经初始化的局部静态变量会被初始化为0;作用域为局部作用域,但离开作用域不被销毁。
- 修饰静态函数:静态函数只能在声明的文件中可见,不能被其他文件引用
- 修饰类的静态成员:在类中,静态成员可以实现多个对象之间的数据共享,静态成员是类的所有对象中共享的成员,而不属于某一个对象;类中的静态成员必须进行显示的初始化
- 修饰类的静态函数:静态函数同类的静态成员变量一个用法,都是属于一个类的方法。而且静态函数中只可以使用类的静态变量。
(2)const:
- 修成类成员:在C++中,const成员变量也不能在类定义处初始化,只能通过构造函数初始化列表进行,并且必须有构造函数;
const数据成员只在某个对象生存期内是常量,而对于整个类而言却是可变的。因为类可以创建多个对象,不同的对象其const数据成员的值可以不同。 - 修饰类函数:该函数中所有变量均不可改变。
9. C++中的const类成员函数(用法和意义),以及和非const成员函数的区别
(1):在赋值方面,const数据成员只能采用初始化列表方式,而非数据成员可以采用初始化列表和构造函数体内赋值两种方式。
class Test
{
public:
Test(int x,int y):m_y(y)
{
m_x = x;//m_x也可以采用初始化列表方式,对于非内部数据类型最好采用初始化列表方式进行初始化
}
private:
int m_x;
const int m_y;
};
(2):在函数调用方面,const成员函数可以访问const数据成员(本身显示不能被修改)和const成员函数,可以访问非const数据成员,但是不能修改非const数据成员,且不能调用非const成员函数,而非const成员函数则没有限制。
- 对于每个对象的成员函数(这里不包括static成员函数,因为其不属于某个对象),其都有一个隐形的参数,也就是指向该类对象的一个指针,普通的成员函数,this指针类型是A(A类)const * this,其可以改变this所指向的值,但是不能修改this所保存的地址。而对于const成员函数,this指针类型是const A * const *this,其既不能改变this所指向的值,也不能修改this所保存的地址,因此上述就很好理解了。
10. C++的顶层const和底层const
- 指针实际定义了两个对象,指针本身和它所指的对象。这两个对象都可以用const进行限定。
- 底层const是代表对象本身是一个常量(不可改变);
const int* p2=&b; //-----可以改变p2的值,这是一个底层const
- 顶层const是代表指针的值是一个常量,而指针的值(即对象的地址)的内容可以改变(指向的不可改变);
int* const p1=&i; //-----不能改变p1的值,这是一个顶层const
11. final和override关键字
(1)final
- 用于限制某个类不能被继承,或者某个虚函数不能被重写,修饰函数,final只能修饰虚函数,并且要放到类或者函数的后面。
- final的用法
struct A
{
//A::foo is final 限定该虚函数不能被重写
virtual void foo() final;
//Error: non-virtual function cannot be final,只能修改虚函数
void bar() final;
};
struct B final : A //struct B is final
{
//Error: foo cannot be overridden as it's final in A
void foo();
};
struct C : B //Error: B is final
{
};
(2)override
- 确保在派生类中声明的重写函数与基类的虚函数有相同的签名,同时也明确表明将会重写基类的虚函数,还可以防止因疏忽把本来想重写基类的虚函数声明成重载。
- 保证重写虚函数的正确性,又提高代码的可读性。关键字要放到方法后面。确认自己目前在子类中正在重写一个来自父类的函数,那么我们最好是用override关键字来修饰该函数,override修饰的函数表示这个函数一定是父类(祖先)中传下来的,这样就帮助我们进行了函数的名称、参数的检查.
- 以后在子类中重写父类函数的时候,一定加上virtual和override关键字
- 代码示例
struct A
{
virtual void func() {
}
};
struct D : A{
//显式重写
void func() override
{
}
};
12. 拷贝初始化和直接初始化
(1)什么是拷贝初始化(也称为复制初始化):将一个已有的对象拷贝到正在创建的对象,如果需要的话还需要进行类型转换。拷贝初始化发生在下列情况:
- 使用赋值运算符定义变量
- 将对象作为实参传递给一个非引用类型的形参
- 将一个返回类型为非引用类型的函数返回一个对象
(2)什么是直接初始化:在对象初始化时,通过括号给对象提供一定的参数,并且要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数
(3)直接初始化和拷贝初始化效率基本一样,因为在底层的实现基本一样,所以将拷贝初始化改为直接初始化效率提高不大。
(4) 例子
- ClassTest ct1(“ab”); 这条语句属于直接初始化,它不需要调用复制构造函数,直接调用构造函数ClassTest(const char *pc),所以当复制构造函数变为私有时,它还是能直接执行的。
- ClassTest ct2 = “ab”; 这条语句为复制初始化,它首先调用构造函数 ClassTest(const char* pc)
函数创建一个临时对象,然后调用复制构造函数,把这个临时对象作为参数,构造对象ct2;所以当复制构造函数变为私有时,该语句不能编译通过。 - ClassTest ct3 = ct1;这条语句为复制初始化,因为 ct1本来已经存在,所以不需要调用相关的构造函数,而直接调用复制构造函数,把它值复制给对象ct3;所以当复制构造函数变为私有时,该语句不能编译通过。
- ClassTest ct4(ct1);这条语句为直接初始化,因为 ct1 本来已经存在,直接调用复制构造函数,生成对象 ct3 的副本对象ct4。所以当复制构造函数变为私有时,该语句不能编译通过。
- 要点就是拷贝初始化和直接初始化调用的构造函数是不一样的,但是当类进行复制时,类会自动生成一个临时的对象,然后再进行拷贝初始化。
13. 初始化和赋值的区别
(1) 普通情况下,初始化和赋值好像没有什么特别去区分它的意义。
- int a=100;
- int a; a=100
(2)复杂数据类型,如类,情况不同;
- 默认构造函数是不传参,构建的对象默认的使用那些值赋值给成员变量;
- 而拷贝构造函数是接受一个相同类的另一个对象,使用该对象来逐成员的为自己的成员赋值;
- 构造函数的目的,是服务于类的初始化的,它并不服务于赋值。赋值是独立于初始化之后的操作。
#include <iostream>
using namespace std;
class Point
{
public:
Point(int a=0, int b=0):x(a), y(b){
};
~Point(){
};
Point& operator =(const Point &rhs);
int x;
int y;
};
Point& Point::operator =(const Point &rhs)
{
x = rhs.x+1;
y = rhs.y+1;
return *this;
}
int main(void)
{
Point p(1,1);
Point p1 = p; //初始化操作
Point p2;
p2 = p; //赋值操作
cout<<"p1.x = "<<p1.x<<" "<<"p1.y="<<p1.y<<endl;
cout<<"p2.x = "<<p2.x<<" "<<"p2.y="<<p2.y<<endl;
return 0;
}
在p1中,Point p1=p;这个操作中,实际上是通过一种类似于拷贝构造函数中逐member的方式(但并没有生成一个拷贝构造函数,生成拷贝构造函数的四种情况见前面的随笔),并没有调用重载的"="运算符。所以最终结果是p1为(1,1)。
而在p2中,初始化与赋值是分开的,Point p2;就已经完成了初始化,这个初始化是通过定义的含参构造函数(但是以a=0,b=0的默认值完成的)。
然后在调用重载运算符,对p中成员均自加后赋值给p2的成员变量。
14. extern "C"的用法
extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,